Speculation Rules API: Pre-renderizado de Páginas Antes de la Navegación e Impacto Medido en Core Web Vitals
La API: Cómo Funciona
Las speculation rules se declaran como un objeto JSON dentro de un tag <script type="speculationrules">:
<script type="speculationrules">
{
"prerender": [
{
"where": {
"and": [
{ "href_matches": "/*" },
{ "not": { "href_matches": "/logout" } },
{ "not": { "href_matches": "/api/*" } },
{ "not": { "selector_matches": ".no-prerender" } }
]
},
"eagerness": "moderate"
}
],
"prefetch": [
{
"where": {
"href_matches": "/blog/*"
},
"eagerness": "conservative"
}
]
}
</script>
Esto le dice a Chrome: "Para cualquier link interno que no sea la página de logout o una ruta de API, pre-renderizala cuando el usuario muestre interés moderado (hover por 200ms). Para links del blog, prefetchealos con intención conservadora (pointerdown — el momento del click)."
La cláusula where soporta pattern matching de URLs (href_matches) y matching de selectores CSS (selector_matches). Puede componer reglas con and, or, y not. Esto es vastamente más poderoso que el viejo <link rel=prerender> que solo podía apuntar a una sola URL.
Niveles de Eagerness: El Espectro de Confianza
Hay cuatro niveles de eagerness, cada uno mapeando a una señal diferente de intención del usuario:
| Eagerness | Trigger | Lead Time Típico | Caso de Uso |
|-----------|---------|-------------------|-------------|
| immediate | Carga de página | Segundos | Navegación siguiente conocida (paso 2 de wizard) |
| eager | Link se hace visible | Segundos | Navegación de alta probabilidad |
| moderate | Hover (200ms) | 200-800ms | Links de navegación general |
| conservative | Pointerdown/touchstart | 50-200ms | Páginas de menor confianza o costosas |
En la práctica, moderate es el punto óptimo para la mayoría de los sitios. Te da 200-800ms de lead time (duración del hover antes del click), que es suficiente para pre-renderizar completamente una página típica.
Instrumenté el comportamiento de hover en un sitio de documentación con 40.000 visitantes diarios:
document.querySelectorAll('a[href]').forEach(link => {
let hoverStart = 0;
link.addEventListener('pointerenter', () => {
hoverStart = performance.now();
});
link.addEventListener('click', () => {
const hoverDuration = performance.now() - hoverStart;
navigator.sendBeacon('/analytics/hover', JSON.stringify({
duration: Math.round(hoverDuration),
url: link.href,
clicked: true,
}));
});
});
Resultados de 2 semanas de datos:
- Mediana de hover-a-click: 380ms
- Percentil 75: 620ms
- Percentil 90: 1.100ms
- Tasa hover-a-click (hover > 200ms): 68% de los hovers resultaron en click
Con eagerness moderate (umbral de 200ms), predecimos correctamente el 68% de las navegaciones. El 32% de prerenders desperdiciados es un costo de ancho de banda, pero Chrome maneja el lifecycle del prerender agresivamente — si la memoria está limitada, desaloja páginas pre-renderizadas.
Speculation Rules Dinámicas
Las reglas estáticas en HTML funcionan para case es simples, pero los sitios reales necesitan reglas dinámicas basadas en comportamiento del usuario, A/B tests, o personalización:
function updateSpeculationRules(urls) {
document.querySelectorAll('script[type="speculationrules"]')
.forEach(el => el.remove());
const script = document.createElement('script');
script.type = 'speculationrules';
script.textContent = JSON.stringify({
prerender: [{
urls: urls.highConfidence,
eagerness: 'moderate',
}],
prefetch: [{
urls: urls.lowConfidence,
eagerness: 'conservative',
}],
});
document.head.appendChild(script);
}
// Predecir próxima página basado en profundidad de scroll
function predictNextPage() {
const scrollPercent = window.scrollY / (document.body.scrollHeight - window.innerHeight);
if (scrollPercent > 0.7) {
const nextLink = document.querySelector('a.pagination-next');
if (nextLink) {
updateSpeculationRules({
highConfidence: [nextLink.href],
lowConfidence: [],
});
}
}
}
window.addEventListener('scroll', debounce(predictNextPage, 500), { passive: true });
Uso este patrón en un blog donde los artículos tienen links de "próximo post". Cuando el usuario scrollea más del 70% del artículo, agrego una regla de prerender para el próximo post. Para cuando llegan abajo y hacen click en "Siguiente," la página ya está completamente renderizada.
Manejando Analytics y Efectos Secundarios
La trampa más grande en producción con prerender son los efectos secundarios. Las páginas pre-renderizadas ejecutan JavaScript. Si tu analytics dispara page_view durante el prerender, se va a contar doble.
Chrome provee la propiedad document.prerendering y el evento prerenderingchange:
function trackPageView() {
if (document.prerendering) {
document.addEventListener('prerenderingchange', () => {
sendPageView();
}, { once: true });
} else {
sendPageView();
}
}
function sendPageView() {
const activationStart = performance.getEntriesByType('navigation')[0]?.activationStart || 0;
gtag('event', 'page_view', {
page_location: window.location.href,
page_title: document.title,
prerendered: activationStart > 0,
activation_delay_ms: activationStart > 0
? Math.round(performance.now() - activationStart)
: 0,
});
}
trackPageView();
Core Web Vitals: El Impacto Medido
Corrí un experimento controlado en un sitio de contenido con ~200.000 visitantes mensuales. Dos semanas sin speculation rules (control), dos semanas con (experimento). Datos CrUX:
Período Control (sin especulación):
| Métrica | p75 | Good (%) | |---------|-----|----------| | LCP | 2.1s | 72% | | INP | 180ms | 85% | | CLS | 0.04 | 94% |
Período Experimento (speculation rules, eagerness moderate):
| Métrica | p75 | Good (%) | |---------|-----|----------| | LCP | 0.8s | 94% | | INP | 95ms | 96% | | CLS | 0.02 | 97% |
LCP bajó de 2.1s a 0.8s en el percentil 75. No es un error de tipeo. Cuando una página está pre-renderizada, la "navegación" es esencialmente swapear un tab oculto al frente. El elemento LCP ya está pintado.
INP mejoró de 180ms a 95ms. Esta me sorprendió. La mejora viene del hecho de que las páginas pre-renderizadas ya completaron el parsing y ejecución inicial de JavaScript. Los event handlers ya están registrados. El main thread está idle cuando el usuario llega.
El costo de ancho de banda fue medible pero razonable: ~15% de aumento en transferencia total de datos por sesión. En promedio, pre-renderizamos 2.3 páginas por sesión, de las cuales 1.6 fueron efectivamente visitadas (68% hit rate).
Optimización de INP: La Conexión con Prerender
INP (Interaction to Next Paint) mide el tiempo desde una interacción del usuario hasta el siguiente update visual. En una navegación tradicional, la primera interacción en una nueva página frecuentemente tiene INP elevado porque el browser todavía está parseando JavaScript y el main thread está ocupado.
Con prerender, todo eso pasa durante la fase oculta de prerender. Para cuando el usuario interactúa con la página activada, el main thread está idle y los event handlers están registrados.
Resultados de INP de primera interacción:
- Sin prerender: Mediana 142ms, p75 215ms, p95 380ms
- Con prerender: Mediana 48ms, p75 82ms, p95 140ms
La mejora del p95 de 380ms a 140ms es significativa. Ese es case es de cola son típicamente causados por páginas con inicialización pesada de JavaScript que bloquea el main thread durante la ventana de primera interacción. Prerender elimina esa clase entera de regresión de INP.
Implementación para Diferentes Frameworks
Para Next.js, agregar speculation rules en tu layout:
// app/layout.js
export default function RootLayout({ children }) {
return (
<html>
<head>
<script
type="speculationrules"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
prerender: [{
where: {
and: [
{ href_matches: "/*" },
{ not: { href_matches: "/api/*" } },
{ not: { href_matches: "/auth/*" } },
],
},
eagerness: "moderate",
}],
}),
}}
/>
</head>
<body>{children}</body>
</html>
);
}
La Speculation Rules API es la mayor mejora individual que hice al rendimiento de navegación percibido en años. La implementación es directa — un blob JSON en tu HTML. El impacto es inmediato y medible en datos CrUX dentro de 28 días. Las trampas con analytics y efectos secundarios son manejables con la API document.prerendering.
Si te importan los Core Web Vitals — y si se está leyendo esto, probablemente sí — esto debería estar en tu lista de implementación este trimestre. La mejora de LCP sola vale el esfuerzo. La mejora de INP es el bonus que lo hace transformador.