diff --git a/Inc/enqueue-scripts.php b/Inc/enqueue-scripts.php
index 5a77be2f..9c812ba2 100644
--- a/Inc/enqueue-scripts.php
+++ b/Inc/enqueue-scripts.php
@@ -17,16 +17,12 @@ if (!defined('ABSPATH')) {
* Estos CSS se cargan con media="print" y onload="this.media='all'"
* para evitar bloquear el renderizado inicial.
*
- * IMPORTANTE: Bootstrap se difiere porque su CSS crítico se inyecta
- * inline via CriticalBootstrapService (~8KB subset en
).
- * El Bootstrap completo (31KB) carga después sin bloquear.
+ * NOTA: Bootstrap NO está diferido porque causa CLS alto (0.954).
+ * Bootstrap debe cargar bloqueante para evitar layout shifts.
*
* @since 1.0.21
- * @since 1.0.22 Added roi-bootstrap to deferred list
*/
define('ROI_DEFERRED_CSS', [
- // Framework CSS (critical subset inline, full deferred)
- 'roi-bootstrap',
// Componentes específicos (below the fold)
'roi-badges',
'roi-pagination',
@@ -99,22 +95,18 @@ add_action('wp_enqueue_scripts', 'roi_enqueue_fonts', 1);
/**
* 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
+ * NOTA: Bootstrap debe cargar BLOQUEANTE (media='all').
+ * Diferirlo causa CLS alto (0.954) por layout shifts.
+ * CriticalBootstrapService queda disponible para futuras optimizaciones.
*/
function roi_enqueue_bootstrap() {
- // Bootstrap CSS - DIFERIDO: critical subset inline, full deferred
- // media='print' + onload cambia a 'all' cuando carga (ver ROI_DEFERRED_CSS)
+ // Bootstrap CSS - BLOQUEANTE para evitar CLS
wp_enqueue_style(
'roi-bootstrap',
get_template_directory_uri() . '/Assets/Vendor/Bootstrap/Css/bootstrap.min.css',
array('roi-fonts'),
'5.3.2',
- 'print' // Diferido - CSS crítico inline via CriticalBootstrapService
+ 'all' // Bloqueante - diferirlo causa CLS alto
);
// Bootstrap Icons CSS - SUBSET OPTIMIZADO (Fase 4.1 PageSpeed)
diff --git a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
index dcd45efc..83ecb097 100644
--- a/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
+++ b/Public/TableOfContents/Infrastructure/Ui/TableOfContentsRenderer.php
@@ -428,12 +428,16 @@ final class TableOfContentsRenderer implements RendererInterface
return '';
}
+ // Intersection Observer elimina forced reflows del ScrollSpy
+ // En lugar de leer offsetTop en cada scroll event (60+/seg),
+ // el navegador notifica solo cuando secciones entran/salen del viewport
$script = <<
document.addEventListener('DOMContentLoaded', function() {
var tocLinks = document.querySelectorAll('.toc-link');
var offsetTop = {$scrollOffset};
+ // Smooth scroll on click - usa getBoundingClientRect una sola vez por click
tocLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
@@ -452,36 +456,93 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
- // ScrollSpy
+ // ScrollSpy con Intersection Observer (sin forced reflows)
var sections = [];
+ var sectionMap = {};
+
tocLinks.forEach(function(link) {
var id = link.getAttribute('href').substring(1);
var section = document.getElementById(id);
if (section) {
- sections.push({ id: id, element: section });
+ sections.push(section);
+ sectionMap[id] = link;
}
});
- function updateActiveLink() {
- var scrollPosition = window.pageYOffset + offsetTop + 50;
- var currentSection = '';
+ if (sections.length === 0) return;
- sections.forEach(function(section) {
- if (section.element.offsetTop <= scrollPosition) {
- currentSection = section.id;
+ // Track de secciones visibles para determinar la activa
+ var visibleSections = new Set();
+ var currentActive = null;
+
+ function updateActiveFromVisible() {
+ if (visibleSections.size === 0) return;
+
+ // Encontrar la sección visible más arriba en el documento
+ var topMostSection = null;
+ var topMostPosition = Infinity;
+
+ visibleSections.forEach(function(id) {
+ var section = document.getElementById(id);
+ if (section) {
+ var rect = section.getBoundingClientRect();
+ if (rect.top < topMostPosition) {
+ topMostPosition = rect.top;
+ topMostSection = id;
+ }
}
});
- tocLinks.forEach(function(link) {
- link.classList.remove('active');
- if (link.getAttribute('href') === '#' + currentSection) {
- link.classList.add('active');
+ if (topMostSection && topMostSection !== currentActive) {
+ // Remover active de todos
+ tocLinks.forEach(function(link) {
+ link.classList.remove('active');
+ });
+
+ // Activar el correspondiente
+ if (sectionMap[topMostSection]) {
+ sectionMap[topMostSection].classList.add('active');
+ currentActive = topMostSection;
}
- });
+ }
}
- window.addEventListener('scroll', updateActiveLink);
- updateActiveLink();
+ // Intersection Observer - el navegador maneja la detección eficientemente
+ var observerOptions = {
+ root: null,
+ rootMargin: '-' + offsetTop + 'px 0px -50% 0px',
+ threshold: 0
+ };
+
+ var observer = new IntersectionObserver(function(entries) {
+ entries.forEach(function(entry) {
+ var id = entry.target.id;
+ if (entry.isIntersecting) {
+ visibleSections.add(id);
+ } else {
+ visibleSections.delete(id);
+ }
+ });
+
+ // Usar requestAnimationFrame para batch DOM updates
+ requestAnimationFrame(updateActiveFromVisible);
+ }, observerOptions);
+
+ // Observar todas las secciones
+ sections.forEach(function(section) {
+ observer.observe(section);
+ });
+
+ // Activar la primera sección visible al cargar
+ requestAnimationFrame(function() {
+ if (visibleSections.size === 0 && sections.length > 0) {
+ var firstId = sections[0].id;
+ if (sectionMap[firstId]) {
+ sectionMap[firstId].classList.add('active');
+ currentActive = firstId;
+ }
+ }
+ });
});
JS;
diff --git a/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php b/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php
index a57674fa..1943db71 100644
--- a/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php
+++ b/Shared/Infrastructure/Wordpress/CriticalCSSHooksRegistrar.php
@@ -4,28 +4,22 @@ declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Wordpress;
use ROITheme\Shared\Infrastructure\Services\CriticalCSSService;
-use ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService;
/**
- * Registra hooks wp_head para inyectar CSS crítico
+ * Registra hook wp_head para inyectar CSS crítico de componentes
*
* RESPONSABILIDAD:
- * - Registrar hook wp_head (priority 0) para Critical Bootstrap
* - Registrar hook wp_head (priority 1) para Critical Component CSS
- * - Delegar renderizado a servicios especializados
+ * - Delegar renderizado a CriticalCSSService
*
* FLUJO:
- * 1. wp_head (priority 0) → CriticalBootstrapService::render()
- * - Lee Assets/css/critical-bootstrap.css (subset ~8KB)
- * - Output:
- *
- * 2. wp_head (priority 1) → CriticalCSSService::render()
+ * 1. wp_head (priority 1) → CriticalCSSService::render()
* - Consulta BD por componentes is_critical=true
* - Genera CSS usando Renderers
* - Output:
*
- * 3. Bootstrap completo se carga diferido (media="print" + onload)
- * - No bloquea renderizado inicial
+ * NOTA: CriticalBootstrapService está DESHABILITADO porque diferir
+ * Bootstrap causa CLS alto (0.954). Bootstrap carga bloqueante.
*
* PATRÓN:
* - SRP: Solo registra hooks, delega lógica a servicios
@@ -37,8 +31,7 @@ use ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService;
final class CriticalCSSHooksRegistrar
{
public function __construct(
- private readonly CriticalCSSService $criticalCSSService,
- private readonly CriticalBootstrapService $criticalBootstrapService
+ private readonly CriticalCSSService $criticalCSSService
) {}
/**
@@ -46,25 +39,10 @@ final class CriticalCSSHooksRegistrar
*/
public function register(): void
{
- // Priority 0 = Critical Bootstrap (primero, antes de componentes)
- add_action('wp_head', [$this, 'renderCriticalBootstrap'], 0);
-
- // Priority 1 = Critical Component CSS (después de Bootstrap)
+ // Priority 1 = Critical Component CSS (hero, navbar, top-notification-bar)
add_action('wp_head', [$this, 'renderCriticalCSS'], 1);
}
- /**
- * Callback para wp_head - Critical Bootstrap
- *
- * Inyecta subset de Bootstrap (~8KB) inline:
- * - Container, flexbox, navbar, dropdown
- * - Output:
- */
- public function renderCriticalBootstrap(): void
- {
- $this->criticalBootstrapService->render();
- }
-
/**
* Callback para wp_head - Critical Component CSS
*
diff --git a/functions-addon.php b/functions-addon.php
index ef2358ef..5d622d87 100644
--- a/functions-addon.php
+++ b/functions-addon.php
@@ -283,28 +283,19 @@ function roi_render_component(string $componentName): string {
* Registra hooks para inyectar CSS crítico en
*
* FLUJO:
- * 1. wp_head (priority 0) → CriticalBootstrapService::render()
- * - Lee Assets/css/critical-bootstrap.css (subset ~8KB)
- * - Output:
- *
- * 2. wp_head (priority 1) → CriticalCSSService::render()
+ * 1. 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:
*
- * 3. Bootstrap completo se carga diferido (media="print" + onload)
- * - Ver Inc/enqueue-scripts.php
+ * NOTA: CriticalBootstrapService está DESHABILITADO porque diferir
+ * Bootstrap causa CLS alto (0.954). Bootstrap carga bloqueante.
*
- * 4. Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline
+ * Cuando los Renderers ejecutan, detectan is_critical y omiten CSS inline.
*/
add_action('after_setup_theme', function() {
$criticalCSSService = roi_get_critical_css_service();
- $criticalBootstrapService = \ROITheme\Shared\Infrastructure\Services\CriticalBootstrapService::getInstance();
-
- $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar(
- $criticalCSSService,
- $criticalBootstrapService
- );
+ $hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService);
$hooksRegistrar->register();
});