fix: eliminate forced reflows in TOC ScrollSpy + revert Bootstrap defer
- Replace scroll event listener with Intersection Observer in TableOfContentsRenderer - Eliminates ~100ms forced reflows from offsetTop reads during scroll - Revert Bootstrap CSS to blocking (media='all') - deferring caused CLS 0.954 - Keep CriticalBootstrapService available for future optimization - Simplify CriticalCSSHooksRegistrar to only use CriticalCSSService 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 <head>).
|
||||
* 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)
|
||||
|
||||
@@ -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 = <<<JS
|
||||
<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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (topMostSection && topMostSection !== currentActive) {
|
||||
// Remover active de todos
|
||||
tocLinks.forEach(function(link) {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === '#' + currentSection) {
|
||||
link.classList.add('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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
JS;
|
||||
|
||||
@@ -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: <style id="roi-critical-bootstrap">...</style>
|
||||
*
|
||||
* 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: <style id="roi-critical-css">...</style>
|
||||
*
|
||||
* 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: <style id="roi-critical-bootstrap">...</style>
|
||||
*/
|
||||
public function renderCriticalBootstrap(): void
|
||||
{
|
||||
$this->criticalBootstrapService->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para wp_head - Critical Component CSS
|
||||
*
|
||||
|
||||
@@ -283,28 +283,19 @@ function roi_render_component(string $componentName): string {
|
||||
* Registra hooks para inyectar CSS crítico en <head>
|
||||
*
|
||||
* FLUJO:
|
||||
* 1. wp_head (priority 0) → CriticalBootstrapService::render()
|
||||
* - Lee Assets/css/critical-bootstrap.css (subset ~8KB)
|
||||
* - Output: <style id="roi-critical-bootstrap">...</style>
|
||||
*
|
||||
* 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: <style id="roi-critical-css">...</style>
|
||||
*
|
||||
* 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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user