Files
roi-theme/Public/CriticalCSS/Infrastructure/Services/CriticalCSSExtractor.php
FrankZamora e01605ec37 feat(critical-css): implementar TIPO 4 y TIPO 5 - CSS Below-the-fold y Lazy Loading
## TIPO 4: CSS Below-the-fold (Critical Variables + Responsive)
- Inyecta variables CSS críticas inline en wp_head P:-1
- Inyecta media queries críticas inline en wp_head P:2 (corregido de P:1)
- Auto-regeneración cuando archivos fuente cambian (filemtime check)
- Cache en Assets/CriticalCSS/ para evitar lecturas repetidas
- Comando WP-CLI: wp roi-theme generate-critical-css

Archivos TIPO 4:
- Public/CriticalCSS/Domain/Contracts/ - Interfaces (DIP)
- Public/CriticalCSS/Application/UseCases/GetCriticalCSSUseCase.php
- Public/CriticalCSS/Infrastructure/Cache/CriticalCSSFileCache.php
- Public/CriticalCSS/Infrastructure/Services/CriticalCSSExtractor.php
- Public/CriticalCSS/Infrastructure/Services/CriticalCSSInjector.php
- bin/generate-critical-css.php

## TIPO 5: CSS No Crítico (Lazy Loading)
- Animaciones CSS: carga 2s después de page load via requestIdleCallback
- Print CSS: carga solo al imprimir via beforeprint event
- Fallback <noscript> para usuarios sin JavaScript
- Safari fallback: setTimeout cuando requestIdleCallback no disponible

Archivos TIPO 5:
- Assets/Js/lazy-css-loader.js
- Public/LazyCSSLoader/Infrastructure/Contracts/LazyCSSRegistrarInterface.php
- Public/LazyCSSLoader/Infrastructure/Services/LazyCSSRegistrar.php

## Fix: Colisión de prioridades wp_head
Antes: TIPO 1 (P:1), TIPO 4 responsive (P:1), TIPO 3 (P:2) - CONFLICTO
Después: TIPO 1 (P:1), TIPO 4 responsive (P:2), TIPO 3 (P:3) - OK

Nuevo orden de prioridades:
P:-1 roi-critical-variables (TIPO 4)
P:0  roi-critical-bootstrap (TIPO 2)
P:1  roi-critical-css (TIPO 1)
P:2  roi-critical-responsive (TIPO 4)
P:3  roi-custom-critical-css (TIPO 3)
P:5  roi-theme-layout-css (ThemeSettings)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 23:06:12 -06:00

148 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Public\CriticalCSS\Infrastructure\Services;
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSCacheInterface;
use ROITheme\Public\CriticalCSS\Domain\Contracts\CriticalCSSExtractorInterface;
/**
* Extrae CSS critico de archivos fuente
*
* Estrategia simple:
* - Variables: Todo el archivo (siempre critico)
* - Responsive: Solo breakpoints mobile-first criticos
*
* @package ROITheme\Public\CriticalCSS\Infrastructure\Services
*/
final class CriticalCSSExtractor implements CriticalCSSExtractorInterface
{
// Archivos fuente
private const SOURCE_VARIABLES = '/Assets/Css/css-global-variables.css';
private const SOURCE_RESPONSIVE = '/Assets/Css/css-global-responsive.css';
// Breakpoints criticos (mobile-first)
// NOTA: El regex maneja 1 nivel de anidamiento.
// Si el CSS tiene @supports dentro de @media, no se capturara correctamente.
// Esto es aceptable para css-global-responsive.css que es simple.
private const CRITICAL_BREAKPOINTS = [
'@media (max-width: 575.98px)',
'@media (min-width: 576px)',
'@media (max-width: 767.98px)',
'@media (min-width: 768px)',
'@media (min-width: 992px)',
];
public function __construct(
private readonly CriticalCSSCacheInterface $cache
) {}
/**
* {@inheritDoc}
*/
public function extractVariables(): string
{
$sourceFile = get_template_directory() . self::SOURCE_VARIABLES;
if (!file_exists($sourceFile)) {
return '';
}
$css = file_get_contents($sourceFile);
if ($css === false) {
return '';
}
// Para variables, todo el contenido es critico
return $this->minify($css);
}
/**
* {@inheritDoc}
*/
public function extractResponsive(): string
{
$sourceFile = get_template_directory() . self::SOURCE_RESPONSIVE;
if (!file_exists($sourceFile)) {
return '';
}
$css = file_get_contents($sourceFile);
if ($css === false) {
return '';
}
$criticalCSS = '';
// Extraer solo media queries criticas
// Regex: captura @media (...) { contenido con 1 nivel de {} }
foreach (self::CRITICAL_BREAKPOINTS as $breakpoint) {
$pattern = '/' . preg_quote($breakpoint, '/') . '\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/s';
if (preg_match_all($pattern, $css, $matches)) {
foreach ($matches[0] as $match) {
$criticalCSS .= $match . "\n";
}
}
}
return $this->minify($criticalCSS);
}
/**
* {@inheritDoc}
*/
public function generateAll(): array
{
$results = [];
// Variables CSS
$variablesCSS = $this->extractVariables();
if (!empty($variablesCSS)) {
$this->cache->set('variables', $variablesCSS);
$results['variables'] = strlen($variablesCSS);
}
// Responsive critico
$responsiveCSS = $this->extractResponsive();
if (!empty($responsiveCSS)) {
$this->cache->set('responsive', $responsiveCSS);
$results['responsive'] = strlen($responsiveCSS);
}
return $results;
}
/**
* {@inheritDoc}
*/
public function needsRegeneration(): bool
{
$templateDir = get_template_directory();
return $this->cache->isStale('variables', $templateDir . self::SOURCE_VARIABLES)
|| $this->cache->isStale('responsive', $templateDir . self::SOURCE_RESPONSIVE);
}
/**
* Minifica CSS eliminando espacios y comentarios innecesarios
*/
private function minify(string $css): string
{
// Eliminar comentarios
$css = preg_replace('/\/\*[\s\S]*?\*\//', '', $css) ?? $css;
// Eliminar espacios innecesarios
$css = preg_replace('/\s+/', ' ', $css) ?? $css;
$css = preg_replace('/\s*([{};:,>+~])\s*/', '$1', $css) ?? $css;
// Eliminar ultimo punto y coma antes de }
$css = preg_replace('/;}/', '}', $css) ?? $css;
return trim($css);
}
}