From 62a0f17b21b28a53bcc16ea7289999acd71c66f1 Mon Sep 17 00:00:00 2001 From: FrankZamora Date: Sat, 29 Nov 2025 12:05:50 -0600 Subject: [PATCH] perf: Defer Bootstrap with inline critical CSS for LCP optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add grid system (row, col-*) to critical-bootstrap.css to prevent CLS - Add text utilities, sizing, spacing, and alert component to critical CSS - Enable CriticalBootstrapService to inline critical Bootstrap in - Defer bootstrap-subset.min.css (21KB) via media=print + onload - Fix preload pointing to wrong Bootstrap file (was 227KB, now 147KB) Expected improvement: ~970ms reduction in render-blocking CSS 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Assets/css/critical-bootstrap.css | 193 +++++++++++++++++++++++++++++- Inc/enqueue-scripts.php | 22 +++- Inc/performance.php | 5 +- functions-addon.php | 17 ++- 4 files changed, 221 insertions(+), 16 deletions(-) diff --git a/Assets/css/critical-bootstrap.css b/Assets/css/critical-bootstrap.css index d489f013..8eb88aed 100644 --- a/Assets/css/critical-bootstrap.css +++ b/Assets/css/critical-bootstrap.css @@ -84,6 +84,78 @@ button:focus:not(:focus-visible) { .container { max-width: 1320px; } } +/* ========================================================================== + GRID SYSTEM (Layout crítico - Previene CLS) + ========================================================================== */ +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-right: calc(-0.5 * var(--bs-gutter-x)); + margin-left: calc(-0.5 * var(--bs-gutter-x)); +} +.row > * { + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-top: var(--bs-gutter-y); +} + +.col { flex: 1 0 0%; } +.col-auto { flex: 0 0 auto; width: auto; } +.col-1 { flex: 0 0 auto; width: 8.33333333%; } +.col-2 { flex: 0 0 auto; width: 16.66666667%; } +.col-3 { flex: 0 0 auto; width: 25%; } +.col-4 { flex: 0 0 auto; width: 33.33333333%; } +.col-5 { flex: 0 0 auto; width: 41.66666667%; } +.col-6 { flex: 0 0 auto; width: 50%; } +.col-7 { flex: 0 0 auto; width: 58.33333333%; } +.col-8 { flex: 0 0 auto; width: 66.66666667%; } +.col-9 { flex: 0 0 auto; width: 75%; } +.col-10 { flex: 0 0 auto; width: 83.33333333%; } +.col-11 { flex: 0 0 auto; width: 91.66666667%; } +.col-12 { flex: 0 0 auto; width: 100%; } + +@media (min-width: 768px) { + .col-md-1 { flex: 0 0 auto; width: 8.33333333%; } + .col-md-2 { flex: 0 0 auto; width: 16.66666667%; } + .col-md-3 { flex: 0 0 auto; width: 25%; } + .col-md-4 { flex: 0 0 auto; width: 33.33333333%; } + .col-md-5 { flex: 0 0 auto; width: 41.66666667%; } + .col-md-6 { flex: 0 0 auto; width: 50%; } + .col-md-7 { flex: 0 0 auto; width: 58.33333333%; } + .col-md-8 { flex: 0 0 auto; width: 66.66666667%; } + .col-md-9 { flex: 0 0 auto; width: 75%; } + .col-md-10 { flex: 0 0 auto; width: 83.33333333%; } + .col-md-11 { flex: 0 0 auto; width: 91.66666667%; } + .col-md-12 { flex: 0 0 auto; width: 100%; } +} + +@media (min-width: 992px) { + .col-lg-1 { flex: 0 0 auto; width: 8.33333333%; } + .col-lg-2 { flex: 0 0 auto; width: 16.66666667%; } + .col-lg-3 { flex: 0 0 auto; width: 25%; } + .col-lg-4 { flex: 0 0 auto; width: 33.33333333%; } + .col-lg-5 { flex: 0 0 auto; width: 41.66666667%; } + .col-lg-6 { flex: 0 0 auto; width: 50%; } + .col-lg-7 { flex: 0 0 auto; width: 58.33333333%; } + .col-lg-8 { flex: 0 0 auto; width: 66.66666667%; } + .col-lg-9 { flex: 0 0 auto; width: 75%; } + .col-lg-10 { flex: 0 0 auto; width: 83.33333333%; } + .col-lg-11 { flex: 0 0 auto; width: 91.66666667%; } + .col-lg-12 { flex: 0 0 auto; width: 100%; } +} + +/* Gutter utilities */ +.g-0, .gx-0 { --bs-gutter-x: 0; } +.g-0, .gy-0 { --bs-gutter-y: 0; } +.g-3, .gx-3 { --bs-gutter-x: 1rem; } +.g-3, .gy-3 { --bs-gutter-y: 1rem; } + /* ========================================================================== FLEXBOX UTILITIES (Layout crítico) ========================================================================== */ @@ -96,10 +168,16 @@ button:focus:not(:focus-visible) { .d-block { display: block !important; } +.d-inline-block { + display: inline-block !important; +} .flex-wrap { flex-wrap: wrap !important; } +.flex-column { + flex-direction: column !important; +} .justify-content-center { justify-content: center !important; @@ -107,31 +185,98 @@ button:focus:not(:focus-visible) { .justify-content-between { justify-content: space-between !important; } +.justify-content-start { + justify-content: flex-start !important; +} +.justify-content-end { + justify-content: flex-end !important; +} .align-items-center { align-items: center !important; } +.align-items-start { + align-items: flex-start !important; +} +.align-items-end { + align-items: flex-end !important; +} .gap-2 { gap: 0.5rem !important; } +.gap-3 { + gap: 1rem !important; +} /* ========================================================================== SPACING UTILITIES (Margin/Padding críticos) ========================================================================== */ +.m-0 { margin: 0 !important; } +.m-auto { margin: auto !important; } + .mb-0 { margin-bottom: 0 !important; } +.mb-1 { margin-bottom: 0.25rem !important; } .mb-2 { margin-bottom: 0.5rem !important; } .mb-3 { margin-bottom: 1rem !important; } +.mb-4 { margin-bottom: 1.5rem !important; } + +.mt-0 { margin-top: 0 !important; } +.mt-2 { margin-top: 0.5rem !important; } +.mt-3 { margin-top: 1rem !important; } .me-1 { margin-right: 0.25rem !important; } .me-2 { margin-right: 0.5rem !important; } +.me-3 { margin-right: 1rem !important; } .ms-2 { margin-left: 0.5rem !important; } .ms-3 { margin-left: 1rem !important; } -.py-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; +.mx-auto { margin-left: auto !important; margin-right: auto !important; } + +.p-0 { padding: 0 !important; } +.p-2 { padding: 0.5rem !important; } +.p-3 { padding: 1rem !important; } + +.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } +.py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } +.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } + +.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; } +.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; } +.px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; } + +/* ========================================================================== + SIZING UTILITIES (Width/Height críticos) + ========================================================================== */ +.w-100 { width: 100% !important; } +.w-auto { width: auto !important; } +.h-100 { height: 100% !important; } +.h-auto { height: auto !important; } + +/* ========================================================================== + TEXT UTILITIES (Críticos para layout) + ========================================================================== */ +.text-center { text-align: center !important; } +.text-start { text-align: left !important; } +.text-end { text-align: right !important; } +.text-white { color: #fff !important; } +.text-muted { color: var(--bs-secondary-color, #6c757d) !important; } + +.fw-normal { font-weight: 400 !important; } +.fw-medium { font-weight: 500 !important; } +.fw-semibold { font-weight: 600 !important; } +.fw-bold { font-weight: 700 !important; } + +.fs-5 { font-size: 1.25rem !important; } +.fs-6 { font-size: 1rem !important; } + +.small { font-size: 0.875em !important; } + +@media (min-width: 768px) { + .text-md-start { text-align: left !important; } + .text-md-center { text-align: center !important; } + .text-md-end { text-align: right !important; } } /* ========================================================================== @@ -384,6 +529,48 @@ button:focus:not(:focus-visible) { .text-decoration-underline { text-decoration: underline !important; } +.text-decoration-none { + text-decoration: none !important; +} + +/* ========================================================================== + IMAGE UTILITIES + ========================================================================== */ +.img-fluid { + max-width: 100%; + height: auto; +} + +/* ========================================================================== + ALERT COMPONENT (Above-the-fold notifications) + ========================================================================== */ +.alert { + --bs-alert-padding-x: 1rem; + --bs-alert-padding-y: 1rem; + --bs-alert-margin-bottom: 1rem; + --bs-alert-border-radius: 0.375rem; + position: relative; + padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x); + margin-bottom: var(--bs-alert-margin-bottom); + border: 1px solid transparent; + border-radius: var(--bs-alert-border-radius); +} +.alert-warning { + --bs-alert-color: #664d03; + --bs-alert-bg: #fff3cd; + --bs-alert-border-color: #ffecb5; + color: var(--bs-alert-color); + background-color: var(--bs-alert-bg); + border-color: var(--bs-alert-border-color); +} +.alert-info { + --bs-alert-color: #055160; + --bs-alert-bg: #cff4fc; + --bs-alert-border-color: #b6effb; + color: var(--bs-alert-color); + background-color: var(--bs-alert-bg); + border-color: var(--bs-alert-border-color); +} /* ========================================================================== BUTTON CLOSE (Dismiss notification) diff --git a/Inc/enqueue-scripts.php b/Inc/enqueue-scripts.php index f26d54df..17cb0369 100644 --- a/Inc/enqueue-scripts.php +++ b/Inc/enqueue-scripts.php @@ -17,12 +17,16 @@ if (!defined('ABSPATH')) { * Estos CSS se cargan con media="print" y onload="this.media='all'" * para evitar bloquear el renderizado inicial. * - * NOTA: Bootstrap NO está diferido porque causa CLS alto (0.954). - * Bootstrap debe cargar bloqueante para evitar layout shifts. + * NOTA: Bootstrap AHORA está diferido porque CriticalBootstrapService + * inyecta las clases críticas (grid, navbar, utilities) inline en . + * Esto permite diferir el bootstrap-subset.min.css sin causar CLS. * * @since 1.0.21 + * @updated 1.0.23 - Bootstrap diferido gracias a critical-bootstrap.css */ define('ROI_DEFERRED_CSS', [ + // Bootstrap (diferido - critical inline via CriticalBootstrapService) + 'roi-bootstrap', // Componentes específicos (below the fold) 'roi-badges', 'roi-pagination', @@ -100,18 +104,24 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_fonts', 1); * - Reducción: 36% menos CSS para parsear * - Generado con PurgeCSS: node build-bootstrap-subset.js * - * NOTA: Bootstrap debe cargar BLOQUEANTE (media='all'). - * Diferirlo causa CLS alto (0.954) por layout shifts. + * NOTA: Bootstrap AHORA es DIFERIDO (media='print' + onload). + * CriticalBootstrapService inyecta inline las clases críticas: + * - Grid system (row, col-*) + * - Navbar component + * - Flexbox utilities + * - Spacing utilities + * Esto evita CLS mientras mejora LCP significativamente. */ function roi_enqueue_bootstrap() { - // Bootstrap CSS SUBSET - BLOQUEANTE para evitar CLS + // Bootstrap CSS SUBSET - DIFERIDO (critical CSS es inline) // Original: 227KB -> Subset: 145KB (36% reducción) + // Critical: ~20KB inline en via CriticalBootstrapService wp_enqueue_style( 'roi-bootstrap', get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css', array('roi-fonts'), '5.3.2-subset', - 'all' // Bloqueante - diferirlo causa CLS alto + 'print' // DIFERIDO - critical CSS inline evita CLS ); // Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed) diff --git a/Inc/performance.php b/Inc/performance.php index 893114b8..8592c3e5 100644 --- a/Inc/performance.php +++ b/Inc/performance.php @@ -336,10 +336,11 @@ add_filter( 'wp_resource_hints', 'roi_add_resource_hints', 10, 2 ); function roi_preload_critical_resources() { $theme_uri = get_template_directory_uri(); - // Preload del CSS de Bootstrap (crítico para el layout) + // Preload del CSS de Bootstrap SUBSET (crítico para el layout) + // IMPORTANTE: Debe coincidir con el archivo encolado en enqueue-scripts.php printf( '' . "\n", - esc_url( $theme_uri . '/Assets/Vendor/Bootstrap/Css/bootstrap.min.css' ) + esc_url( $theme_uri . '/Assets/Vendor/Bootstrap/Css/bootstrap-subset.min.css' ) ); // Preload del CSS de fuentes (crítico para evitar FOIT/FOUT) diff --git a/functions-addon.php b/functions-addon.php index 5d622d87..1cd055d8 100644 --- a/functions-addon.php +++ b/functions-addon.php @@ -283,17 +283,24 @@ function roi_render_component(string $componentName): string { * Registra hooks para inyectar CSS crítico en * * FLUJO: - * 1. wp_head (priority 1) → CriticalCSSService::render() + * 1. wp_head (priority 0) → CriticalBootstrapService::render() + * - Inyecta critical-bootstrap.css inline (grid, navbar, utilities) + * - Permite diferir bootstrap-subset.min.css + * + * 2. wp_head (priority 1) → CriticalCSSService::render() * - Consulta BD por componentes con is_critical=true * - Genera CSS usando los métodos públicos generateCSS() de los Renderers * - Output: * - * NOTA: CriticalBootstrapService está DESHABILITADO porque diferir - * Bootstrap causa CLS alto (0.954). Bootstrap carga bloqueante. - * - * Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline. + * IMPORTANTE: CriticalBootstrapService HABILITADO para mejorar LCP. + * critical-bootstrap.css incluye grid system para evitar CLS. */ add_action('after_setup_theme', function() { + // 1. Critical Bootstrap CSS (priority 0) - inline bootstrap crítico + $criticalBootstrapService = \ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService::getInstance(); + add_action('wp_head', [$criticalBootstrapService, 'render'], 0); + + // 2. Critical Component CSS (priority 1) - CSS de componentes críticos $criticalCSSService = roi_get_critical_css_service(); $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService); $hooksRegistrar->register();