En 2026, las métricas Core Web Vitals dejaron de ser simples pautas de referencia para consolidarse como un filtro algorítmico determinante dentro del sistema Google Page Experience. Aquellos sitios web que no alcanzan el umbral verde en el informe Chrome User Experience Report (CrUX) sufren pérdidas continuadas de visibilidad orgánica en dispositivos móviles y de escritorio frente a rivales con idéntica relevancia temática.
La tríada de Core Web Vitals está compuesta por:
<h1>).FASES CRÍTICAS DEL CICLO DE VIDA DE CORE WEB VITALS:
[ Navigation Start ]
│
├─ TTFB (Time to First Byte)
├─ Resource Load Delay (Tiempo hasta descubrir el recurso LCP)
├─ Resource Load Duration (Tiempo de descarga de AVIF/WebP)
└─ Element Render Delay (Tiempo desde la descarga hasta pintar en CSSOM)
│
▼
[ LCP < 2.5s ] ──► Pintado del contenido principal
│
├─ Font Swap / Size Adjust (Eliminación de FOUT/FOIT)
└─ Image Aspect Ratio Reserved Space
│
▼
[ CLS < 0.1 ] ──► Estabilidad de maquetación sin saltos
│
├─ Input Delay (Cola del hilo principal antes del procesamiento)
├─ Processing Duration (Ejecución de callbacks JS / React Render)
└─ Presentation Delay (Composición y pintado del fotograma por GPU)
│
▼
[ INP < 200ms] ──► Respuesta táctil inmediata de la interfazEn la arquitectura de Next.js 15 App Router, la optimización profunda de estos indicadores se logra mediante la integración sinérgica de tres subsistemas nativos:
next/image: Conversión automática a formatos modernos AVIF/WebP, generación de conjuntos srcset adaptativos, precarga en cabecera mediante <link rel="preload"> con fetchpriority="high" y esqueletos base64 de bajo peso.next/font: Aislamiento integral de fuentes web en el servidor (Zero External Network Requests hacia Google Fonts) y ajuste geométrico de fuentes del sistema mediante los descriptores CSS size-adjust, ascent-override y descent-override.useMemo y useCallback.La optimización de Core Web Vitals en Next.js busca eliminar la contienda de recursos en la Ruta Crítica de Renderizado (Critical Rendering Path) y liberar de tareas pesadas el Hilo Principal (Main Thread) del navegador.
SIN OPTIMIZACIÓN (SPA Tradicional / SSR Antiguo):
Main Thread: [ Parse HTML ]──[ Load GTM (Bloquea) ]──[ React Hydration (Long Task 300ms!) ]──[ UI Freezes ]
Network: [ HTML ]──►[ CSS ]──►[ fonts.gstatic.com ]──►[ Lazy Image 1920px (LCP Retrasado!) ]
Resultado: LCP = 3.8s (Malo), INP = 340ms (Malo), CLS = 0.28 (Salto de texto al cambiar fuente)
CON EL STACK OPTIMIZADO DE NEXT.JS 15:
Main Thread: [ Parse HTML + Shell ]──[ React Compiler (Cortes <16ms) ]──[ Libre para Input de Usuario ]
Worker: └──► Partytown / next/script (worker): Ejecución de GTM y analítica en Web Worker
Network: [ Preload LCP (AVIF) ] (En paralelo con el HTML)
[ Inlined next/font (size-adjust) ] (Alojado en el mismo origin, CLS = 0)
Resultado: LCP = 1.1s (Bueno), INP = 38ms (Bueno), CLS = 0.000 (Impecable)Términos clave de ingeniería de rendimiento:
next/font/google.strategy="worker", dejando el hilo principal expedito para animaciones y scroll.next/image para un LCP Óptimo#La principal causa de degradación en LCP es el retraso en el descubrimiento de la imagen principal junto con la descarga de archivos sobredimensionados.
Para colocar LCP en zona verde, configuramos next.config.ts priorizando el códec AVIF (30–50% más comprimido que WebP) y empleamos next/image con el atributo priority:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
// Priorizar AVIF frente a WebP
formats: ['image/avif', 'image/webp'],
// Límites de pantalla para el srcset adaptativo
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Aumento del TTL de caché en CDN Edge
minimumCacheTTL: 31536000,
},
experimental: {
// Activar React Compiler en Next.js 15
reactCompiler: true,
},
};
export default nextConfig;Implementación de un componente Hero con cálculo preciso de sizes y marcador de posición desenfocado:
import Image from 'next/image';
interface ProductHeroImageProps {
src: string;
alt: string;
/**
* Data URL en base64 de baja resolución generado en el servidor (con sharp o plaiceholder).
* Rellena el contenedor al instante, suprimiendo saltos de interfaz.
*/
blurDataUrl: string;
}
export function ProductHeroImage({ src, alt, blurDataUrl }: ProductHeroImageProps) {
return (
<div className="relative aspect-video w-full max-w-4xl overflow-hidden rounded-2xl border border-border bg-bg-surface">
<Image
src={src}
alt={alt}
fill
// CRUCIAL PARA LCP:
// 1. priority genera <link rel="preload" as="image" fetchpriority="high"> en el <head>
priority
// 2. sizes evita descargar una imagen de 1920px en un móvil de 375px
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 896px"
// 3. blur placeholder ofrece una transición suave y mitiga la sensación de espera
placeholder="blur"
blurDataURL={blurDataUrl}
className="object-cover object-center"
/>
</div>
);
}next/font y size-adjust#Al cargar fuentes con @import o <link>, el navegador renderiza el texto con una tipografía del sistema (como Arial) antes de conmutar a la fuente web. Las diferencias métricas entre caracteres provocan un salto evidente en la disposición de los bloques (alto CLS).
El paquete next/font resuelve este comportamiento de forma automática:
@font-face con la propiedad size-adjust, ajustando las dimensiones de la fuente del sistema al tamaño físico de la fuente personalizada:import { Inter, JetBrains_Mono } from 'next/font/google';
/**
* Fuente tipográfica principal.
* next/font inserta automáticamente definiciones fallback con size-adjust,
* erradicando cualquier salto visual cuando se descarga la fuente web.
*/
export const fontSans = Inter({
subsets: ['latin', 'cyrillic'],
variable: '--font-sans',
display: 'swap',
// adjustFontFallback activado por defecto
adjustFontFallback: true,
preload: true,
});
/**
* Tipografía monoespaciada para bloques de código
*/
export const fontMono = JetBrains_Mono({
subsets: ['latin', 'cyrillic'],
variable: '--font-mono',
display: 'swap',
adjustFontFallback: true,
preload: true,
});Integración de las fuentes en el layout raíz:
import type { Metadata } from 'next';
import { fontSans, fontMono } from '@/app/fonts';
import '@/app/globals.css';
export const metadata: Metadata = {
title: 'Fullstack Agentic Lab Core Web Vitals Masterclass',
description: 'Aplicación web de alto rendimiento construida con Next.js 15',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="es" className={`${fontSans.variable} ${fontMono.variable}`}>
<body className="bg-[#090A0F] text-slate-100 font-sans antialiased min-h-screen">
{children}
</body>
</html>
);
}Los valores altos de INP se deben a tareas prolongadas en el hilo principal (Long Tasks superiores a 50 ms) causadas por re-renderizados extensos durante la interacción del usuario.
En Next.js 15, habilitar experimental: { reactCompiler: true } pone en marcha el React Compiler, que analiza los flujos de datos y aplica memorización granular en las expresiones:
'use client';
import { useState, useTransition } from 'react';
interface ProductItem {
id: string;
name: string;
category: string;
price: number;
}
interface DynamicFilterPanelProps {
initialItems: ProductItem[];
}
export function DynamicFilterPanel({ initialItems }: DynamicFilterPanelProps) {
const [query, setQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [isPending, startTransition] = useTransition();
// NOTA: Con React Compiler los hooks manuales useMemo() y useCallback() son INNECESARIOS.
// El compilador memoriza las operaciones de filtrado y los subárboles JSX resultantes,
// fraccionando la ejecución en bloques breves que no saturan el hilo principal.
const filteredItems = initialItems.filter((item) => {
const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory;
return matchesQuery && matchesCategory;
});
const handleQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
// useTransition clasifica la actualización como interrumpible (manteniendo INP < 50ms),
// otorgando prioridad absoluta a la respuesta visual inmediata de la pulsación de tecla.
startTransition(() => {
setQuery(val);
});
};
return (
<div className="space-y-6">
<div className="flex gap-4">
<input
type="text"
placeholder="Buscar en el catálogo..."
onChange={handleQueryChange}
className="px-4 py-2 bg-bg-surface border border-border rounded-lg text-slate-200 focus:outline-none focus:border-accent-cyan"
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-4 py-2 bg-bg-surface border border-border rounded-lg text-slate-200"
>
<option value="all">Todas las categorías</option>
<option value="servers">Servidores</option>
<option value="storage">Almacenamiento</option>
</select>
{isPending && <span className="text-xs text-accent-cyan self-center">Filtrando...</span>}
</div>
<ul className="grid grid-cols-3 gap-4">
{filteredItems.map((item) => (
<li key={item.id} className="p-4 rounded-lg border border-border bg-bg-surface/60">
<h4 className="font-semibold text-slate-100">{item.name}</h4>
<p className="text-sm text-text-muted mt-1">${item.price.toLocaleString('es-ES')}</p>
</li>
))}
</ul>
</div>
);
}Los scripts de marketing de terceros pueden consumir hasta el 70% del tiempo de CPU, destruyendo el índice INP.
El uso de strategy="worker" en next/script desplaza la ejecución a un Web Worker mediante Partytown:
import Script from 'next/script';
/**
* Aísla los scripts de seguimiento dentro de un Web Worker.
* Mantiene el hilo principal expedito para responder a la interacción del usuario,
* asegurando registros de INP inferiores a 50ms.
*/
export function AnalyticsWorkerScripts() {
return (
<>
{/* Google Tag Manager ejecutado de forma exclusiva en Web Worker */}
<Script
id="gtm-worker-script"
strategy="worker"
src="https://www.googletagmanager.com/gtm.js?id=GTM-AGENTIC2026"
/>
{/* Widgets secundarios pospuestos hasta completar la hidratación */}
<Script
id="support-chat-widget"
strategy="lazyOnload"
src="https://widget.internal.agenticlab.dev/chat.js"
/>
</>
);
}Añadir priority a todas las imágenes del listado con la intención de mejorar LCP desencadena decenas de solicitudes <link rel="preload"> simultáneas con máxima prioridad. La red se congestiona y la imagen Hero principal compite por el ancho de banda con recursos secundarios no visibles.
// ❌ ANTIPATRÓN: ¡Múltiples imágenes compitiendo por la red!
{products.map(p => (
<Image key={p.id} src={p.img} alt={p.name} priority={true} />
))}Solución: Configure priority exclusivamente en una imagen clave dentro del área visible inicial (banner Hero). El resto debe cargarse bajo demanda (comportamiento predeterminado de next/image).
Si se emplea fill omitiendo sizes, el navegador asume un ancho de contenedor de 100vw. Un smartphone de 375px con ratio de píxel 3x solicitará una imagen de 1920px para una miniatura de 120px, empeorando el LCP innecesariamente.
Solución: Declare siempre condiciones de medios descriptivas en sizes que reflejen las dimensiones reales del bloque en el grid.
Importar fuentes con @import url('https://fonts.googleapis.com/...'); en la hoja de estilos genera una costosa cascada de red: se analiza el CSS, se resuelve la regla @import, se solicita la hoja de estilos externa y luego se descargan los archivos WOFF2 desde gstatic. Esto bloquea la visualización inicial y dispara el CLS.
Solución: Utilice siempre next/font/google o next/font/local.
Incluir rastreadores de analítica con la estrategia beforeInteractive en los layouts raíz paraliza el análisis del HTML hasta completar la descarga del script externo. Cualquier retardo en los servidores del proveedor deja al usuario ante una pantalla en blanco, degradando TTFB, LCP e INP.
Solución: Migre las herramientas analíticas hacia estrategias worker o lazyOnload.
Identificación del nodo visual dominante. Asignación de priority y cálculo de sizes adaptativo.
Distribución local de fuentes con sobreescritura de métricas size-adjust para CLS = 0.000.
Activación del compilador de React 19 en next.config.ts para memorización atómica de componentes.
Traslado de scripts analíticos fuera del hilo principal usando next/script con strategy='worker'.
Implementación de métricas Real User Monitoring (RUM) para controlar el percentil 75 en producción.