Optimización de Core Web Vitals en Frontend: De 50 a más de 95 en Lighthouse
Por qué Core Web Vitals impacta directamente en SEO e ingresos
Para 2026, Google ha utilizado Core Web Vitals como señal principal de ranking de búsqueda durante más de tres años. INP (Interaction to Next Paint) reemplazó a FID como la nueva métrica, y las tres métricas—LCP, CLS, INP—determinan directamente si tu sitio obtiene tráfico y conversiones.
| Rendimiento de métricas | Impacto en ranking SEO | Tasa de rebote | Tasa de conversión |
|---|---|---|---|
| Bueno (todas aprueban) | Aumento de ranking 15-30% | Menor al 35% | Mayor al 3.5% |
| Necesita mejoras | Sin cambio significativo | 40-60% | 1.5-3% |
| Pobre (todas fallan) | Caída de ranking 20-40% | Mayor al 70% | Menor al 1% |
Un estudio en sitios de comercio electrónico muestra: cada reducción de 100ms en LCP mejora la conversión en 0.7%; cada reducción de 0.1 en CLS aumenta el tiempo de permanencia del usuario en 15%. El rendimiento ya no es una métrica técnica—es una métrica de negocio.
Estrategias de optimización de LCP
LCP (Largest Contentful Paint) mide el tiempo de renderizado del elemento de contenido más grande. En 2026, el umbral de aprobación es dentro de 2.5 segundos, y el estándar excelente es dentro de 1.8 segundos.
Optimización de imágenes
Las imágenes son típicamente el elemento LCP, y optimizarlas produce los resultados más significativos:
<picture>
<source
srcset="hero-image.avif?w=800 800w, hero-image.avif?w=1200 1200w, hero-image.avif?w=1600 1600w"
type="image/avif"
/>
<source
srcset="hero-image.webp?w=800 800w, hero-image.webp?w=1200 1200w, hero-image.webp?w=1600 1600w"
type="image/webp"
/>
<img
src="hero-image.jpg?w=1200"
srcset="hero-image.jpg?w=800 800w, hero-image.jpg?w=1200 1200w, hero-image.jpg?w=1600 1600w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="Hero banner"
width="1200"
height="600"
fetchpriority="high"
decoding="async"
loading="eager"
/>
</picture>
Puntos clave: AVIF sobre WebP sobre JPEG; usa fetchpriority="high" para aumentar la prioridad de la imagen LCP; siempre declara width y height para prevenir CLS; nunca uses carga diferida en imágenes LCP.
Optimización de fuentes
La carga de fuentes personalizadas es una causa común de retrasos en LCP:
<link
rel="preload"
href="/fonts/inter-var-subset.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-subset.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC;
}
</style>
font-display: swap asegura que el texto siempre sea visible; unicode-range subsetting reduce el tamaño de fuente en 60-80%; preload inicia las solicitudes de fuente tempranamente.
Optimización de CSS
Incluye el CSS crítico en línea, carga el CSS no crítico de forma asíncrona:
<style>
/* Critical CSS - inlined in <head> */
.hero{display:flex;align-items:center;justify-content:center;min-height:60vh;background:linear-gradient(135deg,#667eea,#764ba2)}
.hero h1{font-size:clamp(2rem,5vw,4rem);color:#fff;margin:0}
.hero p{font-size:clamp(1rem,2vw,1.5rem);color:rgba(255,255,255,0.9)}
</style>
<link rel="preload" href="/styles/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles/non-critical.css" /></noscript>
Optimización del servidor
Combinación de CDN + Edge Caching + Streaming:
// Next.js Streaming SSR example
export default async function Page() {
const data = await fetch('https://api.example.com/hero', {
next: { revalidate: 3600 }
}).then(r => r.json());
return (
<main>
<Suspense fallback={<HeroSkeleton />}>
<HeroSection data={data} />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<DeferredContent />
</Suspense>
</main>
);
}
El edge caching devuelve solicitudes LCP en menos de 50ms; Streaming evita que el primer paint se bloquee por datos lentos.
Estrategias de corrección de CLS
CLS (Cumulative Layout Shift) mide la estabilidad visual. En 2026, el umbral de aprobación es dentro de 0.1, y el estándar excelente es dentro de 0.05.
Técnicas de estabilidad de layout
.card {
contain: layout style paint;
content-visibility: auto;
contain-intrinsic-size: 0 320px;
}
.ad-slot {
min-height: 250px;
background: #f5f5f5;
}
.skeleton {
min-height: 200px;
animation: shimmer 1.5s infinite;
}
contain aísla el alcance del impacto del layout; content-visibility: auto omite el renderizado de contenido fuera de pantalla; min-height reserva espacio para contenido dinámico.
Atributos de dimensiones de imagen
<!-- Wrong: missing dimensions -->
<img src="product.jpg" alt="Product" />
<!-- Correct: declare dimensions -->
<img
src="product.jpg"
alt="Product"
width="400"
height="300"
style="width: 100%; height: auto;"
/>
<!-- Responsive aspect ratio -->
<img
src="product.jpg"
alt="Product"
style="aspect-ratio: 4/3; width: 100%; height: auto;"
/>
Manejo de contenido dinámico
function loadComments(postId) {
const container = document.getElementById('comments');
// Reserve minimum height
container.style.minHeight = '300px';
fetch(`/api/comments/${postId}`)
.then(r => r.json())
.then(comments => {
// Batch update to avoid multiple reflows
const fragment = document.createDocumentFragment();
comments.forEach(comment => {
const el = document.createElement('div');
el.className = 'comment-item';
el.textContent = comment.text;
fragment.appendChild(el);
});
container.innerHTML = '';
container.appendChild(fragment);
container.style.minHeight = '';
});
}
Prevención de CLS por carga de fuentes
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-subset.woff2') format('woff2');
font-display: optional;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
/* Or use size-adjust to unify fallback and custom font sizes */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.06%;
ascent-override: 90%;
descent-override: 22%;
}
font-display: optional evita completamente el CLS por intercambio de fuentes; size-adjust alinea el tamaño de la fuente de respaldo con la fuente personalizada.
Estrategias de optimización de INP
INP (Interaction to Next Paint) mide la capacidad de respuesta de las interacciones. En 2026, el umbral de aprobación es dentro de 200ms, y el estándar excelente es dentro de 100ms.
Optimización de manejadores de eventos
// Wrong: synchronous long task blocks main thread
searchInput.addEventListener('input', (e) => {
const results = heavyFilter(e.target.value, allData); // may take 500ms
renderResults(results);
});
// Correct: debounce + yield to main thread
searchInput.addEventListener('input', debounce(async (e) => {
const value = e.target.value;
// Use scheduler.yield to yield to the main thread
await scheduler.yield();
const results = heavyFilter(value, allData);
renderResults(results);
}, 150));
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Gestión del hilo principal
async function processLargeDataset(data) {
const CHUNK_SIZE = 50;
const results = [];
for (let i = 0; i < data.length; i += CHUNK_SIZE) {
const chunk = data.slice(i, i + CHUNK_SIZE);
results.push(...processChunk(chunk));
// Yield to main thread every 50 items
if (i % (CHUNK_SIZE * 10) === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return results;
}
Web Workers para computación pesada
// main.js
const worker = new Worker('/workers/search-worker.js');
worker.postMessage({ type: 'SEARCH', query: userInput, data: largeDataset });
worker.onmessage = (event) => {
if (event.data.type === 'SEARCH_RESULT') {
renderResults(event.data.results);
}
};
// workers/search-worker.js
self.onmessage = (event) => {
if (event.data.type === 'SEARCH') {
const results = performHeavySearch(event.data.query, event.data.data);
self.postMessage({ type: 'SEARCH_RESULT', results });
}
};
function performHeavySearch(query, data) {
return data.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}
requestAnimationFrame y Scheduler API
// 2026 recommended: Scheduler API
async function handleScroll() {
const pendingUpdates = collectScrollUpdates();
// Yield to main thread, ensure next frame paints
await scheduler.yield();
// Execute visual updates at the right time
requestAnimationFrame(() => {
applyScrollUpdates(pendingUpdates);
});
}
// Compatibility fallback
function yieldToMain() {
return new Promise(resolve => {
if ('scheduler' in window && 'yield' in scheduler) {
scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
Configuración de medición y monitoreo
Lighthouse CI
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci && npm run build
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
uploadArtifacts: true
budgetPath: ./lighthouse-budget.json
configPath: ./lighthouserc.json
// lighthouse-budget.json
[
{
"path": "/*",
"options": {
"first-contentful-paint": ["warn", { "maxNumericValue": 1800 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"interactive": ["warn", { "maxNumericValue": 200 }]
}
}
]
Biblioteca web-vitals
import { onLCP, onCLS, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
url: location.href,
timestamp: Date.now()
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', body);
} else {
fetch('/api/vitals', { body, method: 'POST', keepalive: true });
}
}
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
Monitoreo de usuarios reales (RUM)
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
entryType: entry.entryType
});
reportToAnalytics({
metric: entry.name,
value: entry.duration,
path: location.pathname,
connection: navigator.connection?.effectiveType,
deviceMemory: navigator.deviceMemory
});
}
});
observer.observe({
type: 'largest-contentful-paint',
buffered: true
});
observer.observe({ type: 'layout-shift', buffered: true });
observer.observe({ type: 'event', buffered: true });
5 errores comunes y soluciones
Error 1: Uso excesivo de carga diferida
<!-- Wrong: LCP image uses lazy loading -->
<img src="hero.jpg" loading="lazy" alt="Hero" />
<!-- Correct: LCP image uses eager + fetchpriority -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="Hero" />
Nunca uses carga diferida en elementos LCP—hacerlo retrasa el renderizado entre 200-500ms.
Error 2: Ignorar el CLS de fuentes
La carga de fuentes cambia las dimensiones del texto, causando desplazamientos de layout. Siempre usa size-adjust o font-display: optional.
Error 3: Scripts de terceros no optimizados
<!-- Use Partytown to move third-party scripts to a Web Worker -->
<script type="text/partytown" src="https://analytics.example.com/script.js"></script>
<script>
partytown = {
forward: ['dataLayer.push', 'gtag']
};
</script>
Error 4: No manejar desplazamientos de layout por contenido dinámico
Anuncios, listas de recomendaciones y otro contenido asíncrono deben reservar min-height, o cada carga producirá CLS.
Error 5: Ejecutar computación pesada en el hilo principal
Operaciones de búsqueda, ordenamiento y filtrado que excedan 50ms deben moverse a un Web Worker, o INP inevitablemente superará el umbral.
10 elementos de solución de problemas de errores
| # | Síntoma | Causa posible | Método de solución de problemas |
|---|---|---|---|
| 1 | LCP > 4s | Imagen LCP no optimizada | Verificar elemento LCP en el panel Performance de DevTools, comprobar formato y tamaño de imagen |
| 2 | Imagen LCP carga lentamente | Falta de CDN o estrategia de caché | Verificar cache-control y cf-cache-status en Response Headers |
| 3 | CLS > 0.25 | Imágenes sin width/height | Verificar auditoría CLS de Lighthouse para elementos de desplazamiento específicos |
| 4 | CLS por fuentes | font-display mal configurado | Verificar valor de font-display en @font-face |
| 5 | INP > 500ms | Tarea larga en manejador de eventos | Localizar en el panel Long Animation Frames de DevTools |
| 6 | INP con picos ocasionales | Script de terceros bloqueando | Filtrar por URL en el panel Performance para identificar scripts de terceros |
| 7 | FCP normal pero LCP lento | Orden de carga de recursos críticos | Verificar CSS/JS no utilizado en el panel Coverage, cascada en el panel Network |
| 8 | INP móvil mucho peor que escritorio | Problemas de manejo de eventos táctiles | Verificar si touchstart/touchend tienen passive: true |
| 9 | Tartamudeo al desplazar | Manejo pesado de eventos de scroll | Verificar si se usa limitación con requestAnimationFrame |
| 10 | Puntuación de Lighthouse inestable después de optimización | Entorno de prueba inconsistente | Usar Lighthouse CI para condiciones de prueba consistentes |
Caso de estudio real: Página principal de comercio electrónico de 50 a más de 95
Datos de Core Web Vitals antes y después de la optimización para una página principal de comercio electrónico:
| Métrica | Antes | Después | Mejora |
|---|---|---|---|
| Puntuación Lighthouse | 50 | 96 | +92% |
| LCP | 5.2s | 1.4s | -73% |
| FID | 280ms | 45ms | -84% |
| CLS | 0.35 | 0.03 | -91% |
| INP | 450ms | 85ms | -81% |
| TTFB | 1.8s | 0.2s | -89% |
| Tamaño JS primera pantalla | 850KB | 180KB | -79% |
| Tamaño CSS primera pantalla | 320KB | 28KB | -91% |
Pasos de optimización:
- Migración de imágenes a AVIF + CDN: LCP de 5.2s a 3.1s
- CSS crítico en línea + CSS no crítico asíncrono: LCP a 2.3s
- Subsetting de fuentes + font-display: optional: CLS de 0.35 a 0.12
- Atributos de dimensiones de imagen + reserva de min-height: CLS a 0.03
- Lógica de búsqueda migrada a Web Worker: INP de 450ms a 120ms
- scheduler.yield + optimización debounce: INP a 85ms
- Edge Caching + Streaming SSR: TTFB de 1.8s a 0.2s, LCP final 1.4s
Herramientas recomendadas
Las siguientes herramientas en línea pueden ayudarte a trabajar eficientemente durante la optimización:
- Formateador JSON — Formatea y valida datos JSON de reportes Lighthouse para identificar rápidamente problemas de rendimiento
- Codificador Base64 — Convierte iconos pequeños a Base64 en línea para reducir la cantidad de solicitudes HTTP
- Calculadora de Hash — Genera hashes de contenido para recursos estáticos para implementar estrategias de invalidación de caché precisas
Resumen: La optimización de Core Web Vitals no es una tarea única sino un proceso continuo. Desde la optimización de imágenes y fuentes LCP, hasta la estabilidad de layout CLS, hasta la gestión del hilo principal INP, cada métrica requiere una estrategia sistemática. Recuerda: el objetivo de la optimización de rendimiento no es una puntuación perfecta, sino una experiencia fluida para los usuarios. Cuando tu puntuación de Lighthouse pasa de 50 a más de 95, obtienes no solo mejores rankings SEO, sino mejoras reales en la tasa de conversión.
Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →