perf: defer Bootstrap CSS with critical subset inline
- Created Assets/css/critical-bootstrap.css (~10KB subset) Contains only Bootstrap classes used in above-the-fold components: container, navbar, flexbox, dropdown, spacing utilities - Created CriticalBootstrapService (singleton) Injects minified critical Bootstrap in <head> at priority 0 Output: <style id="roi-critical-bootstrap">...</style> - Modified enqueue-scripts.php Bootstrap now loads with media="print" + onload="this.media='all'" Full 31KB Bootstrap loads async, doesn't block rendering - Updated CriticalCSSHooksRegistrar Now registers both CriticalBootstrapService (priority 0) and CriticalCSSService (priority 1) Flow: 1. wp_head (priority 0) → Critical Bootstrap (~10KB inline) 2. wp_head (priority 1) → Critical Component CSS (~4KB inline) 3. Bootstrap full (31KB) loads deferred, non-blocking Expected PageSpeed improvement: ~400-600ms LCP reduction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
470
Assets/css/critical-bootstrap.css
Normal file
470
Assets/css/critical-bootstrap.css
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
/**
|
||||||
|
* Critical Bootstrap CSS Subset
|
||||||
|
*
|
||||||
|
* Contiene SOLO las clases Bootstrap usadas en componentes above-the-fold:
|
||||||
|
* - TopNotificationBar
|
||||||
|
* - Navbar
|
||||||
|
* - Hero
|
||||||
|
*
|
||||||
|
* Esto permite diferir la carga del Bootstrap completo (31KB)
|
||||||
|
* mientras se renderiza el contenido crítico inmediatamente.
|
||||||
|
*
|
||||||
|
* @version 5.3.2-subset
|
||||||
|
* @see Inc/enqueue-scripts.php - Bootstrap diferido
|
||||||
|
* @see Shared/Infrastructure/Services/CriticalBootstrapService.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
BOX SIZING & RESETS (Bootstrap Reboot crítico)
|
||||||
|
========================================================================== */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif);
|
||||||
|
font-size: var(--bs-body-font-size, 1rem);
|
||||||
|
font-weight: var(--bs-body-font-weight, 400);
|
||||||
|
line-height: var(--bs-body-line-height, 1.5);
|
||||||
|
color: var(--bs-body-color, #212529);
|
||||||
|
background-color: var(--bs-body-bg, #fff);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bs-link-color, #0d6efd);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--bs-link-hover-color, #0a58ca);
|
||||||
|
}
|
||||||
|
|
||||||
|
img, svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
CONTAINER (Layout crítico)
|
||||||
|
========================================================================== */
|
||||||
|
.container,
|
||||||
|
.container-fluid {
|
||||||
|
--bs-gutter-x: 1.5rem;
|
||||||
|
--bs-gutter-y: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||||
|
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.container { max-width: 540px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container { max-width: 720px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.container { max-width: 960px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.container { max-width: 1140px; }
|
||||||
|
}
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container { max-width: 1320px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
FLEXBOX UTILITIES (Layout crítico)
|
||||||
|
========================================================================== */
|
||||||
|
.d-flex {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
.d-none {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.d-block {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-content-center {
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
.justify-content-between {
|
||||||
|
justify-content: space-between !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-items-center {
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
SPACING UTILITIES (Margin/Padding críticos)
|
||||||
|
========================================================================== */
|
||||||
|
.mb-0 { margin-bottom: 0 !important; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||||
|
.mb-3 { margin-bottom: 1rem !important; }
|
||||||
|
|
||||||
|
.me-1 { margin-right: 0.25rem !important; }
|
||||||
|
.me-2 { margin-right: 0.5rem !important; }
|
||||||
|
|
||||||
|
.ms-2 { margin-left: 0.5rem !important; }
|
||||||
|
.ms-3 { margin-left: 1rem !important; }
|
||||||
|
|
||||||
|
.py-3 {
|
||||||
|
padding-top: 1rem !important;
|
||||||
|
padding-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
NAVBAR COMPONENT (Crítico - Above the fold)
|
||||||
|
========================================================================== */
|
||||||
|
.navbar {
|
||||||
|
--bs-navbar-padding-x: 0;
|
||||||
|
--bs-navbar-padding-y: 0.5rem;
|
||||||
|
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||||
|
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||||
|
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||||
|
--bs-navbar-active-color: #fff;
|
||||||
|
--bs-navbar-brand-padding-y: 0.3125rem;
|
||||||
|
--bs-navbar-brand-margin-end: 1rem;
|
||||||
|
--bs-navbar-brand-font-size: 1.25rem;
|
||||||
|
--bs-navbar-brand-color: #fff;
|
||||||
|
--bs-navbar-brand-hover-color: #fff;
|
||||||
|
--bs-navbar-nav-link-padding-x: 0.5rem;
|
||||||
|
--bs-navbar-toggler-padding-y: 0.25rem;
|
||||||
|
--bs-navbar-toggler-padding-x: 0.75rem;
|
||||||
|
--bs-navbar-toggler-font-size: 1.25rem;
|
||||||
|
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
||||||
|
--bs-navbar-toggler-focus-width: 0.25rem;
|
||||||
|
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > .container,
|
||||||
|
.navbar > .container-fluid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: inherit;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
padding-top: var(--bs-navbar-brand-padding-y);
|
||||||
|
padding-bottom: var(--bs-navbar-brand-padding-y);
|
||||||
|
margin-right: var(--bs-navbar-brand-margin-end);
|
||||||
|
font-size: var(--bs-navbar-brand-font-size);
|
||||||
|
color: var(--bs-navbar-brand-color);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.navbar-brand:hover,
|
||||||
|
.navbar-brand:focus {
|
||||||
|
color: var(--bs-navbar-brand-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
--bs-nav-link-padding-x: 0;
|
||||||
|
--bs-nav-link-padding-y: 0.5rem;
|
||||||
|
--bs-nav-link-color: var(--bs-navbar-color);
|
||||||
|
--bs-nav-link-hover-color: var(--bs-navbar-hover-color);
|
||||||
|
--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
color: var(--bs-nav-link-color);
|
||||||
|
}
|
||||||
|
.navbar-nav .nav-link:hover,
|
||||||
|
.navbar-nav .nav-link:focus {
|
||||||
|
color: var(--bs-nav-link-hover-color);
|
||||||
|
}
|
||||||
|
.navbar-nav .nav-link.active {
|
||||||
|
color: var(--bs-navbar-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: block;
|
||||||
|
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
|
||||||
|
font-size: var(--bs-nav-link-font-size);
|
||||||
|
font-weight: var(--bs-nav-link-font-weight);
|
||||||
|
color: var(--bs-nav-link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
background: 0 0;
|
||||||
|
border: 0;
|
||||||
|
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);
|
||||||
|
font-size: var(--bs-navbar-toggler-font-size);
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--bs-navbar-color);
|
||||||
|
background-color: transparent;
|
||||||
|
border: var(--bs-border-width, 1px) solid var(--bs-navbar-toggler-border-color);
|
||||||
|
border-radius: var(--bs-navbar-toggler-border-radius);
|
||||||
|
transition: var(--bs-navbar-toggler-transition);
|
||||||
|
}
|
||||||
|
.navbar-toggler:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.navbar-toggler:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-image: var(--bs-navbar-toggler-icon-bg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-dark,
|
||||||
|
.navbar[data-bs-theme="dark"] {
|
||||||
|
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||||
|
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||||
|
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||||
|
--bs-navbar-active-color: #fff;
|
||||||
|
--bs-navbar-brand-color: #fff;
|
||||||
|
--bs-navbar-brand-hover-color: #fff;
|
||||||
|
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
COLLAPSE COMPONENT (Navbar mobile)
|
||||||
|
========================================================================== */
|
||||||
|
.collapse:not(.show) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-collapse {
|
||||||
|
flex-basis: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
DROPDOWN COMPONENT (Navbar submenus)
|
||||||
|
========================================================================== */
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.255em;
|
||||||
|
vertical-align: 0.255em;
|
||||||
|
content: "";
|
||||||
|
border-top: 0.3em solid;
|
||||||
|
border-right: 0.3em solid transparent;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-left: 0.3em solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
--bs-dropdown-zindex: 1000;
|
||||||
|
--bs-dropdown-min-width: 10rem;
|
||||||
|
--bs-dropdown-padding-x: 0;
|
||||||
|
--bs-dropdown-padding-y: 0.5rem;
|
||||||
|
--bs-dropdown-spacer: 0.125rem;
|
||||||
|
--bs-dropdown-font-size: 1rem;
|
||||||
|
--bs-dropdown-color: var(--bs-body-color, #212529);
|
||||||
|
--bs-dropdown-bg: var(--bs-body-bg, #fff);
|
||||||
|
--bs-dropdown-border-color: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||||
|
--bs-dropdown-border-radius: var(--bs-border-radius, 0.375rem);
|
||||||
|
--bs-dropdown-border-width: var(--bs-border-width, 1px);
|
||||||
|
--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius, 0.375rem) - var(--bs-border-width, 1px));
|
||||||
|
--bs-dropdown-divider-bg: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||||
|
--bs-dropdown-divider-margin-y: 0.5rem;
|
||||||
|
--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
--bs-dropdown-link-color: var(--bs-body-color, #212529);
|
||||||
|
--bs-dropdown-link-hover-color: var(--bs-body-color, #212529);
|
||||||
|
--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg, #f8f9fa);
|
||||||
|
--bs-dropdown-link-active-color: #fff;
|
||||||
|
--bs-dropdown-link-active-bg: #0d6efd;
|
||||||
|
--bs-dropdown-link-disabled-color: var(--bs-tertiary-color, #adb5bd);
|
||||||
|
--bs-dropdown-item-padding-x: 1rem;
|
||||||
|
--bs-dropdown-item-padding-y: 0.25rem;
|
||||||
|
--bs-dropdown-header-color: #6c757d;
|
||||||
|
--bs-dropdown-header-padding-x: 1rem;
|
||||||
|
--bs-dropdown-header-padding-y: 0.5rem;
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--bs-dropdown-zindex);
|
||||||
|
display: none;
|
||||||
|
min-width: var(--bs-dropdown-min-width);
|
||||||
|
padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--bs-dropdown-font-size);
|
||||||
|
color: var(--bs-dropdown-color);
|
||||||
|
text-align: left;
|
||||||
|
list-style: none;
|
||||||
|
background-color: var(--bs-dropdown-bg);
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
|
||||||
|
border-radius: var(--bs-dropdown-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--bs-dropdown-link-color);
|
||||||
|
text-align: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.dropdown-item:hover,
|
||||||
|
.dropdown-item:focus {
|
||||||
|
color: var(--bs-dropdown-link-hover-color);
|
||||||
|
background-color: var(--bs-dropdown-link-hover-bg);
|
||||||
|
}
|
||||||
|
.dropdown-item.active,
|
||||||
|
.dropdown-item:active {
|
||||||
|
color: var(--bs-dropdown-link-active-color);
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: var(--bs-dropdown-link-active-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
TEXT UTILITIES
|
||||||
|
========================================================================== */
|
||||||
|
.text-decoration-underline {
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
BUTTON CLOSE (Dismiss notification)
|
||||||
|
========================================================================== */
|
||||||
|
.btn-close {
|
||||||
|
--bs-btn-close-color: #000;
|
||||||
|
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||||
|
--bs-btn-close-opacity: 0.5;
|
||||||
|
--bs-btn-close-hover-opacity: 0.75;
|
||||||
|
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
--bs-btn-close-focus-opacity: 1;
|
||||||
|
--bs-btn-close-disabled-opacity: 0.25;
|
||||||
|
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
padding: 0.25em 0.25em;
|
||||||
|
color: var(--bs-btn-close-color);
|
||||||
|
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
opacity: var(--bs-btn-close-opacity);
|
||||||
|
}
|
||||||
|
.btn-close:hover {
|
||||||
|
color: var(--bs-btn-close-color);
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: var(--bs-btn-close-hover-opacity);
|
||||||
|
}
|
||||||
|
.btn-close:focus {
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: var(--bs-btn-close-focus-shadow);
|
||||||
|
opacity: var(--bs-btn-close-focus-opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close-white {
|
||||||
|
filter: var(--bs-btn-close-white-filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
RESPONSIVE BREAKPOINTS (navbar-expand-lg)
|
||||||
|
========================================================================== */
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.navbar-expand-lg {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.navbar-expand-lg .navbar-nav {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.navbar-expand-lg .navbar-nav .dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.navbar-expand-lg .navbar-nav .nav-link {
|
||||||
|
padding-right: var(--bs-navbar-nav-link-padding-x);
|
||||||
|
padding-left: var(--bs-navbar-nav-link-padding-x);
|
||||||
|
}
|
||||||
|
.navbar-expand-lg .navbar-collapse {
|
||||||
|
display: flex !important;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
.navbar-expand-lg .navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-lg-block { display: block !important; }
|
||||||
|
.d-lg-none { display: none !important; }
|
||||||
|
.mb-lg-0 { margin-bottom: 0 !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.navbar-expand-lg > .container,
|
||||||
|
.navbar-expand-lg > .container-fluid {
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
RESPONSIVE DISPLAY UTILITIES (md breakpoint)
|
||||||
|
========================================================================== */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.d-md-block { display: block !important; }
|
||||||
|
.d-md-none { display: none !important; }
|
||||||
|
}
|
||||||
@@ -17,9 +17,16 @@ if (!defined('ABSPATH')) {
|
|||||||
* Estos CSS se cargan con media="print" y onload="this.media='all'"
|
* Estos CSS se cargan con media="print" y onload="this.media='all'"
|
||||||
* para evitar bloquear el renderizado inicial.
|
* para evitar bloquear el renderizado inicial.
|
||||||
*
|
*
|
||||||
|
* IMPORTANTE: Bootstrap se difiere porque su CSS crítico se inyecta
|
||||||
|
* inline via CriticalBootstrapService (~8KB subset en <head>).
|
||||||
|
* El Bootstrap completo (31KB) carga después sin bloquear.
|
||||||
|
*
|
||||||
* @since 1.0.21
|
* @since 1.0.21
|
||||||
|
* @since 1.0.22 Added roi-bootstrap to deferred list
|
||||||
*/
|
*/
|
||||||
define('ROI_DEFERRED_CSS', [
|
define('ROI_DEFERRED_CSS', [
|
||||||
|
// Framework CSS (critical subset inline, full deferred)
|
||||||
|
'roi-bootstrap',
|
||||||
// Componentes específicos (below the fold)
|
// Componentes específicos (below the fold)
|
||||||
'roi-badges',
|
'roi-badges',
|
||||||
'roi-pagination',
|
'roi-pagination',
|
||||||
@@ -91,15 +98,23 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_fonts', 1);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue Bootstrap 5 styles and scripts
|
* Enqueue Bootstrap 5 styles and scripts
|
||||||
|
*
|
||||||
|
* OPTIMIZACIÓN PageSpeed: Bootstrap CSS se carga diferido.
|
||||||
|
* El CSS crítico (container, navbar, flexbox) se inyecta inline
|
||||||
|
* via CriticalBootstrapService antes de que Bootstrap cargue.
|
||||||
|
*
|
||||||
|
* @see Shared/Infrastructure/Services/CriticalBootstrapService.php
|
||||||
|
* @see Assets/css/critical-bootstrap.css
|
||||||
*/
|
*/
|
||||||
function roi_enqueue_bootstrap() {
|
function roi_enqueue_bootstrap() {
|
||||||
// Bootstrap CSS - with high priority
|
// Bootstrap CSS - DIFERIDO: critical subset inline, full deferred
|
||||||
|
// media='print' + onload cambia a 'all' cuando carga (ver ROI_DEFERRED_CSS)
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'roi-bootstrap',
|
'roi-bootstrap',
|
||||||
get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap.min.css',
|
get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap.min.css',
|
||||||
array('roi-fonts'),
|
array('roi-fonts'),
|
||||||
'5.3.2',
|
'5.3.2',
|
||||||
'all'
|
'print' // Diferido - CSS crítico inline via CriticalBootstrapService
|
||||||
);
|
);
|
||||||
|
|
||||||
// Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed)
|
// Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed)
|
||||||
|
|||||||
148
Shared/Infrastructure/Services/CriticalBootstrapService.php
Normal file
148
Shared/Infrastructure/Services/CriticalBootstrapService.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ROITheme\Shared\Infrastructure\Services;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CriticalBootstrapService
|
||||||
|
*
|
||||||
|
* Inyecta CSS crítico de Bootstrap en wp_head para evitar
|
||||||
|
* bloqueo de renderizado por el Bootstrap completo.
|
||||||
|
*
|
||||||
|
* RESPONSABILIDAD:
|
||||||
|
* - Leer critical-bootstrap.css (subset de Bootstrap)
|
||||||
|
* - Inyectar en <head> via wp_head (priority 0, antes de CriticalCSSService)
|
||||||
|
* - Cachear contenido para evitar lecturas repetidas
|
||||||
|
*
|
||||||
|
* FLUJO:
|
||||||
|
* 1. wp_head (priority 0) → render()
|
||||||
|
* 2. Lee Assets/css/critical-bootstrap.css
|
||||||
|
* 3. Output: <style id="roi-critical-bootstrap">...</style>
|
||||||
|
*
|
||||||
|
* IMPORTANTE:
|
||||||
|
* - Este servicio se ejecuta ANTES de CriticalCSSService (priority 0 vs 1)
|
||||||
|
* - Bootstrap completo se carga diferido (media="print" + onload)
|
||||||
|
*
|
||||||
|
* @package ROITheme\Shared\Infrastructure\Services
|
||||||
|
*/
|
||||||
|
final class CriticalBootstrapService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Instancia singleton
|
||||||
|
*/
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache del contenido CSS
|
||||||
|
*/
|
||||||
|
private ?string $cssCache = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruta al archivo CSS crítico
|
||||||
|
*/
|
||||||
|
private string $cssFilePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor privado (singleton)
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$this->cssFilePath = get_template_directory() . '/Assets/css/critical-bootstrap.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene la instancia singleton
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function getInstance(): self
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderiza Critical Bootstrap CSS en wp_head
|
||||||
|
*
|
||||||
|
* Este método se llama desde wp_head con priority 0 (muy temprano).
|
||||||
|
* Inyecta el subset de Bootstrap antes del CSS de componentes.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function render(): void
|
||||||
|
{
|
||||||
|
$css = $this->getCriticalCSS();
|
||||||
|
|
||||||
|
if (empty($css)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minificar CSS removiendo comentarios y espacios innecesarios
|
||||||
|
$minifiedCSS = $this->minifyCSS($css);
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<style id="roi-critical-bootstrap">%s</style>' . "\n",
|
||||||
|
$minifiedCSS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene el contenido del CSS crítico
|
||||||
|
*
|
||||||
|
* @return string CSS crítico o string vacío si no existe
|
||||||
|
*/
|
||||||
|
private function getCriticalCSS(): string
|
||||||
|
{
|
||||||
|
// Usar cache si existe
|
||||||
|
if ($this->cssCache !== null) {
|
||||||
|
return $this->cssCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que el archivo existe
|
||||||
|
if (!file_exists($this->cssFilePath)) {
|
||||||
|
error_log('ROI Theme: Critical Bootstrap CSS file not found: ' . $this->cssFilePath);
|
||||||
|
$this->cssCache = '';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leer contenido
|
||||||
|
$content = file_get_contents($this->cssFilePath);
|
||||||
|
|
||||||
|
if ($content === false) {
|
||||||
|
error_log('ROI Theme: Failed to read Critical Bootstrap CSS');
|
||||||
|
$this->cssCache = '';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cssCache = $content;
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minifica CSS removiendo comentarios y espacios innecesarios
|
||||||
|
*
|
||||||
|
* @param string $css CSS a minificar
|
||||||
|
* @return string CSS minificado
|
||||||
|
*/
|
||||||
|
private function minifyCSS(string $css): string
|
||||||
|
{
|
||||||
|
// Remover comentarios
|
||||||
|
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
|
||||||
|
|
||||||
|
// Remover espacios, tabs y newlines
|
||||||
|
$css = str_replace(["\r\n", "\r", "\n", "\t"], '', $css);
|
||||||
|
|
||||||
|
// Remover espacios múltiples
|
||||||
|
$css = preg_replace('/\s+/', ' ', $css);
|
||||||
|
|
||||||
|
// Remover espacios alrededor de caracteres especiales
|
||||||
|
$css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);
|
||||||
|
|
||||||
|
// Remover último punto y coma antes de }
|
||||||
|
$css = str_replace(';}', '}', $css);
|
||||||
|
|
||||||
|
return trim($css);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,22 +4,31 @@ declare(strict_types=1);
|
|||||||
namespace ROITheme\Shared\Infrastructure\Wordpress;
|
namespace ROITheme\Shared\Infrastructure\Wordpress;
|
||||||
|
|
||||||
use ROITheme\Shared\Infrastructure\Services\CriticalCSSService;
|
use ROITheme\Shared\Infrastructure\Services\CriticalCSSService;
|
||||||
|
use ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registra hook wp_head para inyectar CSS crítico
|
* Registra hooks wp_head para inyectar CSS crítico
|
||||||
*
|
*
|
||||||
* RESPONSABILIDAD:
|
* RESPONSABILIDAD:
|
||||||
* - Registrar hook wp_head (priority 1)
|
* - Registrar hook wp_head (priority 0) para Critical Bootstrap
|
||||||
* - Delegar renderizado a CriticalCSSService
|
* - Registrar hook wp_head (priority 1) para Critical Component CSS
|
||||||
|
* - Delegar renderizado a servicios especializados
|
||||||
*
|
*
|
||||||
* FLUJO:
|
* FLUJO:
|
||||||
* 1. wp_head (priority 1) → renderCriticalCSS()
|
* 1. wp_head (priority 0) → CriticalBootstrapService::render()
|
||||||
* 2. CriticalCSSService consulta BD por componentes is_critical=true
|
* - Lee Assets/css/critical-bootstrap.css (subset ~8KB)
|
||||||
* 3. Genera CSS usando Renderers y lo inyecta en <head>
|
* - Output: <style id="roi-critical-bootstrap">...</style>
|
||||||
* 4. Los Renderers detectan is_critical y omiten CSS inline
|
*
|
||||||
|
* 2. wp_head (priority 1) → CriticalCSSService::render()
|
||||||
|
* - Consulta BD por componentes is_critical=true
|
||||||
|
* - Genera CSS usando Renderers
|
||||||
|
* - Output: <style id="roi-critical-css">...</style>
|
||||||
|
*
|
||||||
|
* 3. Bootstrap completo se carga diferido (media="print" + onload)
|
||||||
|
* - No bloquea renderizado inicial
|
||||||
*
|
*
|
||||||
* PATRÓN:
|
* PATRÓN:
|
||||||
* - SRP: Solo registra hook, delega lógica a CriticalCSSService
|
* - SRP: Solo registra hooks, delega lógica a servicios
|
||||||
*
|
*
|
||||||
* UBICACIÓN: Infrastructure/Wordpress
|
* UBICACIÓN: Infrastructure/Wordpress
|
||||||
*
|
*
|
||||||
@@ -28,7 +37,8 @@ use ROITheme\Shared\Infrastructure\Services\CriticalCSSService;
|
|||||||
final class CriticalCSSHooksRegistrar
|
final class CriticalCSSHooksRegistrar
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CriticalCSSService $criticalCSSService
|
private readonly CriticalCSSService $criticalCSSService,
|
||||||
|
private readonly CriticalBootstrapService $criticalBootstrapService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,12 +46,27 @@ final class CriticalCSSHooksRegistrar
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
// Priority 1 = muy temprano en <head>, antes de otros estilos
|
// Priority 0 = Critical Bootstrap (primero, antes de componentes)
|
||||||
|
add_action('wp_head', [$this, 'renderCriticalBootstrap'], 0);
|
||||||
|
|
||||||
|
// Priority 1 = Critical Component CSS (después de Bootstrap)
|
||||||
add_action('wp_head', [$this, 'renderCriticalCSS'], 1);
|
add_action('wp_head', [$this, 'renderCriticalCSS'], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback para wp_head
|
* Callback para wp_head - Critical Bootstrap
|
||||||
|
*
|
||||||
|
* Inyecta subset de Bootstrap (~8KB) inline:
|
||||||
|
* - Container, flexbox, navbar, dropdown
|
||||||
|
* - Output: <style id="roi-critical-bootstrap">...</style>
|
||||||
|
*/
|
||||||
|
public function renderCriticalBootstrap(): void
|
||||||
|
{
|
||||||
|
$this->criticalBootstrapService->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback para wp_head - Critical Component CSS
|
||||||
*
|
*
|
||||||
* Ejecuta CriticalCSSService que:
|
* Ejecuta CriticalCSSService que:
|
||||||
* - Consulta BD por componentes con is_critical=true
|
* - Consulta BD por componentes con is_critical=true
|
||||||
|
|||||||
@@ -280,18 +280,31 @@ function roi_render_component(string $componentName): string {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registra el hook para inyectar CSS crítico en <head>
|
* Registra hooks para inyectar CSS crítico en <head>
|
||||||
*
|
*
|
||||||
* FLUJO:
|
* FLUJO:
|
||||||
* 1. wp_head (priority 1) → CriticalCSSService::render()
|
* 1. wp_head (priority 0) → CriticalBootstrapService::render()
|
||||||
* 2. CriticalCSSService consulta BD por componentes con is_critical=true
|
* - Lee Assets/css/critical-bootstrap.css (subset ~8KB)
|
||||||
* 3. Genera CSS usando los métodos públicos generateCSS() de los Renderers
|
* - Output: <style id="roi-critical-bootstrap">...</style>
|
||||||
* 4. Output: <style id="roi-critical-css">...</style>
|
*
|
||||||
* 5. Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline
|
* 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: <style id="roi-critical-css">...</style>
|
||||||
|
*
|
||||||
|
* 3. Bootstrap completo se carga diferido (media="print" + onload)
|
||||||
|
* - Ver Inc/enqueue-scripts.php
|
||||||
|
*
|
||||||
|
* 4. Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline
|
||||||
*/
|
*/
|
||||||
add_action('after_setup_theme', function() {
|
add_action('after_setup_theme', function() {
|
||||||
$criticalCSSService = roi_get_critical_css_service();
|
$criticalCSSService = roi_get_critical_css_service();
|
||||||
$hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService);
|
$criticalBootstrapService = \ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService::getInstance();
|
||||||
|
|
||||||
|
$hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar(
|
||||||
|
$criticalCSSService,
|
||||||
|
$criticalBootstrapService
|
||||||
|
);
|
||||||
$hooksRegistrar->register();
|
$hooksRegistrar->register();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user