El marco de evaluación de experiencia de usuario de Google — Core Web Vitals (CWV) — constituye un factor de posicionamiento directo en los algoritmos de búsqueda. En la especificación vigente de 2026, las métricas fundamentales son:
En las SPAs cliente tradicionales, las métricas CWV sufren severas degradaciones por limitaciones estructurales:
fetch() a la API para traer datos y solo entonces inicia la carga de la imagen principal. Esto alarga el LCP a 4–7 segundos en redes móviles lentas.La anatomía de la latencia de interacción (INP) se divide en tres fases secuenciales:
INP = Input Delay + Processing Time + Presentation Delay
Interacción de Usuario (Clic / Toque)
|
v
+-----------------------------------------------------------------------+
| 1. Input Delay (Retraso de Entrada) |
| Espera por la finalización de Long Tasks en el hilo principal |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| 2. Processing Time (Tiempo de Procesamiento) |
| Ejecución de manejadores onClick, actualización de estado en React |
+-----------------------------------------------------------------------+
|
v
+-----------------------------------------------------------------------+
| 3. Presentation Delay (Retraso de Presentación) |
| Recalculate Style -> Layout -> Paint -> Composición de Fotograma |
+-----------------------------------------------------------------------+
|
v
Fotograma Dibujado en Pantalla (Next Paint)La neutralización de Long Tasks se fundamenta en ceder voluntariamente el control al planificador del navegador (Yielding to Main Thread). Si una operación pesada de 150 ms se divide en tres fragmentos de 40 ms empleando el API nativo scheduler.yield(), el navegador puede intercalar el renderizado de fotogramas y la atención a interacciones entre dichos bloques:
Tarea Monolítica (INP > 150 ms):
+-------------------------------------------------------------+
| Task (150 ms) [Bloquea Entrada de Usuario] |
+-------------------------------------------------------------+
Fragmentación con scheduler.yield() (INP < 50 ms):
+--------------+ +--------------+ +--------------+
| Fragmento 1 | yield | Fragmento 2 | yield | Fragmento 3 |
| (40 ms) | -------> | (40 ms) | -------> | (40 ms) |
+--------------+ +--------------+ +--------------+
^ ^
| |
[Procesa Clic] [Pinta Pantalla]useTransition y scheduler.yield() al ordenar listas en cliente para conservar la latencia de teclado por debajo de 16 ms.<link rel="preload" as="image"> antes de la ejecución del bundle JS.aspect-ratio y contain-intrinsic-size para aislar cualquier salto de diseño (CLS) durante el montaje tardío de scripts de terceros.@font-face con font-display: optional o ajuste de métricas con size-adjust para eliminar movimientos abruptos de texto al cargar fuentes personalizadas.scheduler.yield() y React startTransition#El estándar web scheduler.yield() permite pausar ejecuciones prolongadas devolviendo el control al hilo principal sin perder la prioridad de la tarea original. A continuación se detalla su polyfill de alto rendimiento y una función de procesamiento por lotes.
declare global {
interface Window {
scheduler?: {
yield?: () => Promise<void>;
};
}
}
// Utilidad universal para ceder el hilo principal al navegador
export async function yieldToMain(): Promise<void> {
if (typeof window !== 'undefined' && window.scheduler?.yield) {
return window.scheduler.yield();
}
// Fallback con MessageChannel (menor sobrecarga y latencia que setTimeout(fn, 0))
return new Promise((resolve) => {
const channel = new MessageChannel();
channel.port1.onmessage = () => resolve();
channel.port2.postMessage(null);
});
}
// Procesamiento por bloques conscientes del presupuesto de fotogramas (16 ms)
export async function chunkedDataProcessing<T, R>(
items: readonly T[],
processItem: (item: T) => R,
maxTimePerChunkMs: number = 16
): Promise<R[]> {
const results: R[] = [];
let lastYieldTime = performance.now();
for (let i = 0; i < items.length; i++) {
results.push(processItem(items[i]));
// Si el bloque excede 16 ms (1 fotograma a 60 FPS), cede el control al navegador
if (performance.now() - lastYieldTime > maxTimePerChunkMs) {
await yieldToMain();
lastYieldTime = performance.now();
}
}
return results;
}useTransition y useDeferredValue para Optimizar INP#Al escribir en un campo de búsqueda, la actualización síncrona inmediata de colecciones pesadas desploma el INP. Desacoplar las prioridades con useTransition asegura que el teclado responda sin ningún retraso perceptible.
import React, { useState, useTransition, useDeferredValue, ChangeEvent } from 'react';
interface Product {
id: string;
name: string;
category: string;
}
interface FilterProps {
products: readonly Product[];
}
export function FastCatalogFilter({ products }: FilterProps) {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
// Valor diferido para cálculos secundarios de listado
const deferredQuery = useDeferredValue(query);
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const nextValue = e.target.value;
// Actualización urgente: el texto escrito se pinta inmediatamente
setQuery(nextValue);
// Transición de baja prioridad: el filtrado no bloquea la entrada de texto
startTransition(() => {
// Los cálculos pesados ceden el paso a nuevas pulsaciones de tecla
});
};
const filteredProducts = products.filter((p) =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<div className="space-y-4">
<div className="relative">
<input
type="search"
value={query}
onChange={handleInputChange}
placeholder="Buscar productos al instante..."
className="w-full px-4 py-2 rounded-lg border border-border bg-surface text-slate-100 focus:outline-none focus:border-accent-cyan"
/>
{isPending && (
<span className="absolute right-3 top-2.5 text-xs text-accent-violet animate-pulse">
Filtrando...
</span>
)}
</div>
<ul className="divide-y divide-border border border-border rounded-lg max-h-96 overflow-y-auto">
{filteredProducts.slice(0, 100).map((product) => (
<li key={product.id} className="p-3 text-sm hover:bg-surface/50">
{product.name}
</li>
))}
</ul>
</div>
);
}En una SPA, el banner principal (Elemento LCP) no debe esperar a que se descargue, analice y ejecute main-bundle.js. La referencia al recurso crítico debe incluirse de manera anticipada en el <head> del HTML inicial.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 1. Precarga anticipada del bundle principal -->
<link rel="modulepreload" href="/assets/main-bundle.js" />
<!-- 2. Precarga prioritaria de la imagen LCP principal -->
<link
rel="preload"
as="image"
href="https://images.agenticlab.dev/hero-banner-1200.webp"
type="image/webp"
fetchpriority="high"
/>
<!-- 3. Preconexión al origen de la API -->
<link rel="preconnect" href="https://api.agenticlab.dev" crossorigin />
<link rel="dns-prefetch" href="https://api.agenticlab.dev" />
<style>
/* 4. CSS inline crítico para estabilizar el contenedor héroe */
.hero-container {
aspect-ratio: 16 / 9;
background-color: #09090b;
contain: layout paint;
width: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>aspect-ratio#Cualquier componente asíncrono o multimedia dinámico (anuncios, reproductores de vídeo, banners) debe contar con una reserva dimensional inmutable en el CSS.
import React, { useState } from 'react';
interface StableMediaProps {
src: string;
alt: string;
width: number;
height: number;
}
export function StableMediaContainer({ src, alt, width, height }: StableMediaProps) {
const [isLoaded, setIsLoaded] = useState(false);
return (
// La relación aspect-ratio previene cualquier desplazamiento de maquetación
<div
className="relative overflow-hidden rounded-xl bg-slate-900 border border-slate-800"
style={{
aspectRatio: `${width} / ${height}`,
minHeight: '200px',
contain: 'layout paint',
}}
>
<img
src={src}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
onLoad={() => setIsLoaded(true)}
className={`w-full h-full object-cover transition-opacity duration-300 ${
isLoaded ? 'opacity-100' : 'opacity-0'
}`}
/>
{!isLoaded && (
<div className="absolute inset-0 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 animate-pulse" />
)}
</div>
);
}Librerías como Google Tag Manager y píxeles publicitarios ejecutadas de forma síncrona acaparan el hilo principal durante los primeros 3 segundos de carga. Si el usuario intenta interactuar en ese lapso, el INP se dispara a 400–800 ms.
Solución: Delegar la analítica en Web Workers mediante Partytown o posponer su ejecución hasta el evento requestIdleCallback.
Si la imagen principal carece de prioridad explícita o incluye por descuido loading="lazy", el navegador posterga su descarga dando preferencia a fuentes y estilos secundarios. Esto añade segundos innecesarios a la métrica LCP.
Solución: Marcar el elemento LCP con fetchpriority="high" y jamás usar loading="lazy" en elementos ubicados en la primera pantalla.
Un contenedor publicitario con height: auto se expande repentinamente tras ejecutarse la red de anuncios. El contenido se desplaza 250px hacia abajo mientras el usuario lee, produciendo un salto drástico de CLS > 0.25.
Solución: Encapsular los bloques de publicidad en contenedores con altura mínima fija precalculada: min-height: 280px; display: block;.
El valor font-display: block oculta el texto hasta que la fuente se descarga por completo (Flash of Invisible Text), deteriorando el LCP. A su vez, font-display: swap sin igualación dimensional causa un salto brusco de texto al sustituirse la fuente del sistema (alto CLS).
Solución: Emplear font-display: optional para que solo se use la fuente si está disponible en menos de 100 ms, o calibrar métricas con size-adjust, ascent-override y descent-override.
El navegador interpreta link rel=preload en el HTML estático, descargando la imagen en paralelo al bundle JS.
Los estilos fijan aspect-ratio y contain: layout paint, garantizando coordenadas exactas en pantalla.
Las funciones complejas se dividen en ciclos menores a 16 ms alternando con scheduler.yield().
La entrada del usuario se prioriza garantizando renderizado inmediato del campo de texto.