Optimización de rendimiento frontend: guía completa de Core Web Vitals

Lighthouse en 60 puntos y una semana para subir a 90. Abres Chrome DevTools y tienes que entender qué significan LCP, FID y CLS. Para los desarrolladores frontend, optimizar el rendimiento suele ser trabajo extra que aparece de repente.
He tropezado con varias trampas: la primera vez optimicé todas las imágenes con lazy loading y el LCP bajó — la imagen grande del primer pantallazo no debe cargarse en diferido. Otra vez pasé dos días con la estrategia de caché de Service Worker y la puntuación solo subió 2 puntos. Este artículo no es teoría: es una guía práctica para ver resultados en 2 semanas — qué optimizaciones tienen mejor ROI, qué errores evitar y cómo priorizar para pasar de 60 a 90+ en Lighthouse.
Capítulo 1: Entender las tres métricas de Core Web Vitals
No te asustes por las siglas en inglés. En resumen: ¿carga rápido?, ¿responde al clic?, ¿la pantalla se mueve?
Qué son Core Web Vitals
Google definió en 2020 tres métricas de experiencia de usuario que no solo afectan a quien visita tu sitio, sino también al SEO. Por muy bueno que sea el contenido, un rendimiento pobre hunde el ranking. En marzo de 2024 hubo un cambio importante: INP sustituyó a FID como métrica central. Si sigues optimizando FID, vas tarde.
LCP - Largest Contentful Paint
LCP mide la velocidad con la que carga el contenido principal de la página, normalmente la imagen grande del primer pantallazo, el título o un vídeo. Los umbrales son:
- Menos de 2,5 s: bueno (verde)
- 2,5-4 s: necesita mejora (amarillo)
- Más de 4 s: malo (rojo)
Me gusta compararlo con un restaurante: LCP es la velocidad a la que traen el plato principal. Si esperas 10 minutos y aún no llega, te frustras, ¿verdad?
Dato real: si el tiempo de carga pasa de 3 a 5 segundos, la tasa de rebote sube un 38 %. En móvil, si tarda más de 3 s, el 53 % de los usuarios se va. Optimizar LCP puede subir la conversión entre un 7 y un 15 %.
INP - Interaction to Next Paint
Es la métrica nueva de marzo de 2024, que sustituyó oficialmente a FID. INP mide la velocidad con la que la página responde a la interacción del usuario: clic, teclado o toque, en todo el ciclo de respuesta. Los umbrales son:
- Menos de 200 ms: bueno (verde)
- 200-500 ms: necesita mejora (amarillo)
- Más de 500 ms: malo (rojo)
¿Por qué Google cambió FID por INP? FID solo medía el retraso de la primera entrada; INP cubre todo el proceso de interacción. Como pedir en un restaurante: FID solo mira si el camarero te oyó; INP mira desde que pides hasta que llega el plato.
La primera vez vi un INP de 650 ms en un proyecto: un botón tardaba más de medio segundo. Resultó ser un embed automático de YouTube; al quitarlo, INP bajó a 220 ms y la fluidez se notó al instante.
CLS - Cumulative Layout Shift
CLS mide la estabilidad visual de la página. ¿Te ha pasado ir a pulsar un botón y que la página salte y acabes en un anuncio? Eso es CLS. Los umbrales son:
- Menos de 0,1: bueno (verde)
- 0,1-0,25: necesita mejora (amarillo)
- Más de 0,25: malo (rojo)
La primera vez vi un CLS de 0,5 y pensé que era bajo; luego supe que ya era zona crítica. Problemas habituales: imágenes sin ancho/alto, anuncios que se insertan de golpe, parpadeo de fuentes (FOIT).
Capítulo 2: Optimizar LCP — el mayor ROI
¿Por qué LCP va primero?
- Impacto directo: el usuario lo percibe al instante
- Mucho margen: un LCP de 5 s puede bajar de 2 s con facilidad
- Soluciones maduras: optimización de imágenes, CDN, etc.
- ROI máximo: poco esfuerzo, resultado rápido
Flujo completo de optimización de Core Web Vitals
Plan completo de LCP, INP y CLS para subir Lighthouse de 60 a 90+ en 2 semanas
Estimated time: PT2W
-
1
Step 1: Prioridad P0: optimización de imágenes (LCP)
Las imágenes representan más del 70 % de los problemas de LCP; el ROI es el más alto: -
2
Step 2: Prioridad P0: retrasar scripts de terceros (INP)
Los scripts de terceros son el cuello de botella principal de INP: -
3
Step 3: Prioridad P0: ancho y alto en imágenes (CLS)
CLS es la métrica más fácil de arreglar: -
4
Step 4: Prioridad P1: code splitting y recursos
Code splitting y optimización de recursos: -
5
Step 5: Prioridad P1: fuentes y servidor
Fuentes y servidor: -
6
Step 6: Prioridad P2: optimización avanzada
Optimización avanzada (ROI medio, mayor dificultad):
1. Optimización de imágenes (ROI: ⭐⭐⭐⭐⭐)
Es el punto más importante: más del 70 % de los problemas de LCP. Siempre empiezo por aquí.
a) Formatos modernos
No subestimes el formato: pasar de JPEG a AVIF puede ahorrar 300 KB por imagen.
<!-- Opción 1: etiqueta <picture>, el navegador elige el mejor formato -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image"> <!-- fallback para navegadores antiguos -->
</picture>
// Opción 2: optimización automática en Next.js (recomendado)
import Image from 'next/image'
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // Importante: la imagen LCP debe llevar priority, ¡sin lazy loading!
/>
Datos reales:
- AVIF comprime un 41 % más que JPEG
- WebP comprime un 30 % más que JPEG
- Caso real: en la home de un e-commerce, hero de 500 KB a 120 KB (AVIF), LCP de 4,2 s a 2,1 s — ¡la mitad!
b) Lazy loading (excepto la imagen LCP)
Trampa que yo mismo pisé: lazy loading también en la imagen LCP y la puntuación bajó.
<!-- ❌ Mal: ¡no hagas lazy loading en la imagen LCP! -->
<img src="hero.jpg" loading="lazy">
<!-- ✅ Bien: eager en el hero, lazy solo en el resto -->
<img src="hero.jpg" loading="eager"> <!-- imagen LCP -->
<img src="product1.jpg" loading="lazy"> <!-- imágenes más abajo -->
<img src="product2.jpg" loading="lazy">
Regla: ninguna imagen visible en el primer pantallazo en lazy loading. El lazy es para lo que está debajo del fold.
c) Imágenes responsivas
En móvil no hace falta la imagen de escritorio; srcset puede ahorrar un 60 % de tráfico.
<img
src="hero-800w.jpg"
srcset="hero-400w.jpg 400w,
hero-800w.jpg 800w,
hero-1200w.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
alt="Hero"
/>
Efecto real: en móvil cargar 400w en lugar de 1200w mejora el LCP ~1 s.
d) CDN de imágenes y preload
Un CDN no es lujo, es necesario. OSS de Alibaba cuesta pocos euros al año.
<!-- Preload de imagen clave para priorizar la descarga -->
<link rel="preload" as="image" href="hero.jpg">
<!-- CDN de imágenes (conversión de formato + compresión) -->
<!-- Tencent COS, Alibaba OSS, Cloudflare Images, etc. -->
<img src="https://cdn.example.com/hero.jpg?x-oss-process=image/format,webp/quality,80">
e) Tamaño y compresión
Herramientas:
- TinyPNG: compresión online, directa
- ImageOptim: imprescindible en Mac, por lotes
- Squoosh: de Google, soporta AVIF
Reglas: - Hero del primer pantallazo: calidad 80 % (casi imperceptible)
- Resto: 70 %
- Ancho al doble del diseño (pantallas retina)
f) Evitar Base64 inline en imágenes grandes
// ❌ No inlinees imágenes grandes (más de 10 KB)
// Aumenta el HTML y retrasa el render
const heroImage = 'data:image/jpeg;base64,/9j/4AAQSkZJRg...' // 500 KB
// ✅ Base64 solo para iconos pequeños (menos de 10 KB)
// p. ej. spinner, SVG simple
const icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR...' // 2 KB
2. Tiempo de respuesta del servidor (ROI: ⭐⭐⭐⭐)
a) Usar CDN
Todo estático en CDN; el HTML también puede cachearse (con SSG/ISR). He visto TTFB de 200 ms a 50 ms: se nota.
b) SSR o SSG
// Next.js - generación estática (preferida, mejor rendimiento)
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // ISR: regenerar cada 60 s
}
}
// O renderizado en servidor (si los datos deben ser en tiempo real)
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
c) Consultas a base de datos
- Índices (no olvides EXPLAIN)
- Redis para datos calientes
- Evita N+1 (join o dataloader)
3. Carga de recursos (ROI: ⭐⭐⭐)
a) CSS crítico inline
<!-- CSS crítico del primer pantallazo en <head> -->
<style>
.hero {
width: 100%;
height: 600px;
background: #f0f0f0;
}
.nav {
position: fixed;
top: 0;
width: 100%;
}
</style>
<!-- CSS no crítico en diferido -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
b) Fuentes
/* font-display evita FOIT (Flash of Invisible Text) */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Muestra fallback al instante */
}
<!-- Preload de fuente clave -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
Capítulo 3: Optimizar INP — interacciones más fluidas
¡Los scripts de terceros son el peor enemigo del INP! Lo más extremo que vi: 12 scripts en una página — Analytics, Facebook Pixel, chat, anuncios… INP por encima de 800 ms.
1. Ejecución de JavaScript (ROI: ⭐⭐⭐⭐)
a) Code splitting
Suena sofisticado, pero es cargar bajo demanda: solo el código de la ruta que visitas.
// React - división por rutas
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('./Dashboard'))
const Profile = lazy(() => import('./Profile'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
}
// Vue 3 - componentes asíncronos
const Dashboard = defineAsyncComponent(() => import('./Dashboard.vue'))
Efecto real: panel de admin de 1,2 MB a 200 KB en primer pantallazo, INP de 450 ms a 180 ms.
b) Trocear tareas largas
Más de 50 ms bloqueando el hilo principal es tarea larga y dispara el INP.
// ❌ Tarea larga que bloquea (se congela)
function processLargeData(data) {
for (let i = 0; i < 10000; i++) {
heavyCalculation(data[i])
}
}
// ✅ requestIdleCallback en trozos pequeños
function processLargeData(data) {
let index = 0
function processChunk() {
const chunkSize = 100
let count = 0
while (index < data.length && count < chunkSize) {
heavyCalculation(data[index])
index++
count++
}
if (index < data.length) {
requestIdleCallback(processChunk)
}
}
requestIdleCallback(processChunk)
}
c) Web Workers
Cálculos pesados en el Worker, sin bloquear el hilo principal.
// worker.js
self.onmessage = (e) => {
const result = complexCalculation(e.data)
self.postMessage(result)
}
// main.js
const worker = new Worker('worker.js')
worker.postMessage(data)
worker.onmessage = (e) => {
console.log('Result:', e.data)
}
2. Scripts de terceros (ROI: ⭐⭐⭐⭐⭐)
Cada script compite por recursos.
a) Carga diferida de scripts no críticos
<!-- ❌ Bloquea el render -->
<script src="analytics.js"></script>
<!-- ✅ Tras parsear el HTML -->
<script defer src="analytics.js"></script>
<!-- ✅ O retraso manual de 3 s -->
<script>
window.addEventListener('load', () => {
setTimeout(() => {
const script = document.createElement('script')
script.src = 'analytics.js'
document.body.appendChild(script)
}, 3000)
})
</script>
b) Facade para componentes pesados
Un embed de YouTube pesa más de 1 MB y arrastra el INP.
// Portada ligera; el vídeo real solo al clic
function VideoFacade({ videoId }) {
const [showVideo, setShowVideo] = useState(false)
if (!showVideo) {
return (
<div
className="video-facade"
style={{
backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
cursor: 'pointer'
}}
onClick={() => setShowVideo(true)}
>
<button className="play-button">▶ Reproducir vídeo</button>
</div>
)
}
return <iframe src={`https://www.youtube.com/embed/${videoId}`} />
}
Efecto real: quitar embed automático de YouTube, INP de 650 ms a 220 ms.
3. Manejo de eventos (ROI: ⭐⭐⭐)
a) Debounce y throttle
import { debounce, throttle } from 'lodash'
const handleSearch = debounce((value) => {
fetchSearchResults(value)
}, 300)
const handleScroll = throttle(() => {
updateScrollPosition()
}, 100)
b) Listeners passive
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('touchmove', handleTouch, { passive: true })
Capítulo 4: Optimizar CLS — evitar saltos visuales
CLS es como que alguien empuje el libro mientras lees: tienes que buscar la línea otra vez. La buena noticia: es lo más fácil de arreglar.
1. Dimensiones en imágenes y vídeo (ROI: ⭐⭐⭐⭐⭐)
Sin ancho/alto es la causa número uno de CLS, y también la más simple de corregir.
<!-- ❌ Sin dimensiones: salta al cargar -->
<img src="photo.jpg" alt="Photo">
<!-- ✅ width y height reservan espacio -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- ✅ O aspect-ratio en CSS -->
<style>
img {
width: 100%;
aspect-ratio: 16 / 9;
}
</style>
Caso real: un sitio de noticias puso ancho/alto en todas las imágenes; CLS de 0,35 a 0,05.
2. Carga de fuentes (ROI: ⭐⭐⭐⭐)
a) font-display: swap
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
b) Preload de fuentes clave
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
c) Fuentes del sistema o Variable Font
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
@font-face {
font-family: 'Inter';
src: url('Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
}
3. Espacio reservado para contenido dinámico (ROI: ⭐⭐⭐⭐)
a) Hueco para anuncios
.ad-container {
min-height: 250px;
background: #f0f0f0;
}
b) Skeleton screen
No solo es estética: estabiliza el layout.
function ProductCard({ loading, data }) {
if (loading) {
return (
<div className="skeleton">
<div className="skeleton-image" style={{ width: '100%', height: '200px', background: '#e0e0e0' }} />
<div className="skeleton-title" style={{ width: '80%', height: '20px', background: '#e0e0e0', margin: '10px 0' }} />
<div className="skeleton-price" style={{ width: '40%', height: '20px', background: '#e0e0e0' }} />
</div>
)
}
return (
<div className="product">
<img src={data.image} alt={data.title} />
<h3>{data.title}</h3>
<p>{data.price}</p>
</div>
)
}
4. No insertar contenido encima del existente (ROI: ⭐⭐⭐⭐⭐)
// ❌ Banner arriba empuja todo (CLS alto)
<div>
{showBanner && <Banner />}
<Content />
</div>
// ✅ fixed no mueve el layout
<div>
{showBanner && <Banner style={{ position: 'fixed', top: 0, zIndex: 1000 }} />}
<Content style={{ marginTop: showBanner ? '60px' : '0' }} />
</div>
5. Animar con transform, no top/left (ROI: ⭐⭐⭐)
/* ❌ Provoca layout y CLS */
.element {
position: relative;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { top: -100px; }
to { top: 0; }
}
/* ✅ Solo composite (GPU) */
.element {
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { transform: translateY(-100px); }
to { transform: translateY(0); }
}
Capítulo 5: Flujo completo en la práctica
La prioridad importa: no empieces por SSR si aún no optimizaste imágenes. La primera vez, solo poner ancho/alto en imágenes subió 10 puntos — puntos gratis.
Matriz de prioridades
| Optimización | ROI | Dificultad | Prioridad |
|---|---|---|---|
| Ancho/alto en imágenes | Muy alto | Muy baja | P0 |
| Conversión de formato | Muy alto | Baja | P0 |
| Imagen LCP | Muy alto | Baja | P0 |
| Scripts de terceros en diferido | Muy alto | Baja | P0 |
| Fuentes | Alto | Baja | P1 |
| Code splitting | Alto | Media | P1 |
| CDN | Alto | Baja | P1 |
| SSR/SSG | Medio | Alta | P2 |
| Web Workers | Bajo | Alta | P3 |
Herramientas y comandos
# 1. Lighthouse (referencia)
# Chrome DevTools > Lighthouse > Generate report
# Modo incógnito para evitar extensiones
# 2. WebPageTest (dispositivos reales)
# https://webpagetest.org
# 3. Panel Performance de Chrome DevTools
# 4. Análisis de bundles
npx webpack-bundle-analyzer
# 5. Análisis de imágenes
npx sharp-cli info image.jpg
Errores a evitar
- ❌ Lighthouse en producción sin modo incógnito o extensiones desactivadas
- ❌ Una sola medición (al menos 3 y media; la red fluctúa)
- ❌ Solo escritorio (móvil importa más: ~75 % del tráfico)
- ❌ Sin throttling de red (simula Fast 3G)
- ❌ Lazy loading en la imagen LCP (trampa clásica)
Conclusión
La optimización de rendimiento no es un one-shot: es un hábito, como el deporte. Cuando Lighthouse pasa de 60 a 90, la satisfacción es real.
No es solo técnica: es respeto por quien usa tu sitio. Cada segundo que quitas retiene más gente. En móvil la paciencia ronda los 3 s; tú puedes acercarte a 1 s.
Que cada visita se sienta fluida.
FAQ
¿Cuáles son los umbrales de las tres métricas de Core Web Vitals?
• Menos de 2,5 s bueno; 2,5-4 s mejorable; más de 4 s malo
INP (Interaction to Next Paint):
• Menos de 200 ms bueno; 200-500 ms mejorable; más de 500 ms malo
• Desde marzo de 2024 INP sustituyó a FID
CLS (Cumulative Layout Shift):
• Menos de 0,1 bueno; 0,1-0,25 mejorable; más de 0,25 malo
Datos clave:
• Carga de 3 a 5 s: +38 % de rebote
• Móvil >3 s: se va el 53 % de usuarios
• Buen LCP: +7-15 % de conversión
¿Cómo optimizar LCP (Largest Contentful Paint)?
1) Formatos modernos (AVIF +41 %, WebP +30 % vs JPEG)
• Caso: hero 500 KB→120 KB, LCP 4,2 s→2,1 s
2) Imagen LCP con priority, sin lazy loading; eager en el primer pantallazo
3) srcset responsivo (~60 % menos tráfico)
4) CDN y preload de imágenes clave
5) TTFB (CDN, SSR/SSG, consultas DB)
Prioridad: imágenes, máximo ROI.
¿Cómo optimizar INP (Interaction to Next Paint)?
1) Code splitting por rutas (React.lazy, Vue defineAsyncComponent)
• Caso: 1,2 MB→200 KB primer pantallazo, INP 450 ms→180 ms
2) Scripts de terceros con defer o retraso 3 s; Facade en componentes pesados
3) Quitar embeds automáticos (YouTube: 650 ms→220 ms)
4) Trocear tareas con requestIdleCallback
5) Web Workers para cálculo pesado
6) Debounce, throttle y listeners passive
¿Cómo optimizar CLS (Cumulative Layout Shift)?
1) width/height o aspect-ratio en imágenes
• Caso: CLS 0,35→0,05
2) Fuentes: font-display:swap, preload, sistema o Variable Font
3) Espacio reservado (min-height en anuncios, skeleton)
4) No insertar arriba del contenido (fixed)
5) Animaciones con transform, no top/left
¿Cuál es el orden de prioridad en optimización de rendimiento?
• Ancho/alto en imágenes
• AVIF/WebP
• LCP con priority, sin lazy
• Scripts de terceros en diferido
P1 (ROI alto):
• Fuentes (font-display:swap)
• Code splitting
• CDN
P2 (ROI medio, alta dificultad):
• SSR/SSG
P3 (ROI bajo, alta dificultad):
• Web Workers
Empieza por P0: solo ancho/alto puede sumar 10 puntos. No vayas a SSR antes de las imágenes.
¿Se puede usar lazy loading en la imagen LCP?
La imagen LCP (hero) debe llevar priority o loading="eager". El lazy es solo fuera del primer pantallazo.
Mal:
<img src="hero.jpg" loading="lazy"> — baja el LCP
Bien:
• eager en el hero; lazy en el resto
• Ninguna imagen visible al cargar en lazy
¿Cómo medir y probar Core Web Vitals?
1) Lighthouse (DevTools; incógnito sin extensiones)
2) WebPageTest (dispositivos y regiones)
3) Performance en DevTools (grabación de carga)
4) webpack-bundle-analyzer (tamaño de paquetes)
5) sharp-cli info (metadatos de imagen)
Evita:
• Producción sin control de extensiones
• Una sola prueba (media de 3+)
• Solo desktop (móvil ~75 % tráfico)
• Sin throttling (Fast 3G)
12 min de lectura · Publicado el: 24 nov 2025 · Actualizado el: 21 ago 2026
Framework frontend
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Vue 3 + TypeScript: mejores prácticas — guía de arquitectura empresarial 2025
Guía completa 2025 de arquitectura empresarial con Vue 3 + TypeScript. Cubre inicialización con Vite, Pinia, definición de tipos en TypeScript, ESLint 9 y más, con configuraciones listas para usar.
Parte 5 de 6
Siguiente
Este es el artículo más reciente de la serie por ahora.



Comentarios
Inicia sesión con GitHub para dejar un comentario