Rendimiento Frontend: Core Web Vitals sin mitos — Qué mueve la aguja

¿Te fue útil?

Has desplegado el rediseño. Al equipo de producto le encanta. Luego los paneles se encienden: Core Web Vitals “Necesita mejoras”, la conversión baja y soporte reporta “se siente lento” como si fuera un único bug que puedes buscar con grep.

Aquí es donde los equipos pierden semanas puliendo la piedra equivocada. Core Web Vitals son medibles, pero no mágicos. Trátalos como cualquier SLO de producción: entiende qué está fallando realmente, aísla el cuello de botella y despliega la mínima corrección que cambie la curva.

Core Web Vitals en términos sencillos (sin humos)

Core Web Vitals (CWV) son un conjunto pequeño de métricas de rendimiento centradas en el usuario. No son la “salsa secreta de Google”. Son una forma más o menos estándar de describir: (1) cuándo aparece contenido significativo, (2) cuán estable está la página mientras carga, y (3) qué tan responsiva se siente cuando los usuarios intentan hacer algo.

LCP: Largest Contentful Paint (carga)

LCP responde: “¿Cuándo vio el usuario lo principal?” Normalmente una imagen principal, un gran encabezado o el bloque de contenido superior. No es “cuando aparece el spinner”. No es “DOMContentLoaded”. Es el elemento de contenido visible más grande en el viewport.

Asesinos comunes de LCP:

  • TTFB alto (origen lento, fallos de caché, SSR costoso, mala configuración del CDN)
  • CSS que bloquea el renderizado
  • Imágenes hero no priorizadas (lazy-loaded por encima del pliegue, sin preload, bytes enormes)
  • Renderizado del lado cliente que retrasa el contenido hasta que JS se ejecuta
  • Scripts de terceros que roban la main thread temprano

CLS: Cumulative Layout Shift (estabilidad visual)

CLS responde: “¿Se movió la página bajo el cursor del usuario?” Los shifts de layout son esos saltos molestos cuando imágenes, anuncios o fuentes que cargan tarde empujan el contenido. CLS no trata sobre animación; trata sobre movimientos inesperados.

Asesinos comunes de CLS:

  • Imágenes/iframes sin width/height (o sin aspect-ratio)
  • Banners inyectados (consentimiento de cookies, cintas promocionales) que empujan contenido
  • Cambios tardíos de fuentes que refluye el texto
  • Anuncios/widgets que cambian de tamaño después de cargar

INP: Interaction to Next Paint (responsividad)

INP responde: “Cuando el usuario interactúa, ¿cuánto tarda hasta que la UI se actualiza?” Reemplazó a FID porque a los usuarios no les importa solo la primera interacción; les importa que el sitio sea responsivo durante toda la sesión. INP es sensible a tareas largas, contención de la main thread y manejadores de eventos costosos.

Traducción operativa: LCP es mayormente un problema de canal de entrega (red + recursos críticos). CLS es mayormente un problema de disciplina de layout. INP es mayormente un problema de planificación de CPU y limpieza de código.

Una cita, porque aplica también en web perf: Idea parafraseada de John Ousterhout: la complejidad es la raíz de muchos problemas de software; reducirla mejora la fiabilidad y el rendimiento.

Broma #1: El trabajo de rendimiento es como hacer dieta: la gente jura por trucos raros, pero “comer menos calorías” sigue ganando.

Qué realmente mueve la aguja: una pila de prioridades

La mayoría de equipos pierden tiempo porque tratan CWV como una lista de micro-optimización. No lo hagas. Trátalo como respuesta a incidentes: identifica la restricción dominante, arréglala, re-mide, repite.

1) Arregla la ruta crítica antes de tocar micro-optimizaciones

Si tu HTML tarda 800ms en llegar, recortar 20ms de un bundle JS es teatro. Tu ruta crítica es:

  1. DNS/TCP/TLS (a veces oculto por CDNs, a veces no)
  2. TTFB del HTML + tamaño del HTML
  3. CSS crítico y recursos que bloquean el render
  4. Imagen hero (prioridad + bytes)
  5. Hydration / ejecución JS (para híbridos SPA/SSR)

Mueve el LCP arreglando la primera restricción lenta.

2) Cachea en serio (y verifica)

“Usamos un CDN” no es lo mismo que “nuestro HTML y assets hero se sirven desde caché para usuarios reales”. CWV es centrado en el usuario; si el 40% de los usuarios falla la caché por cookies, cabeceras Vary o comportamiento geográfico, tus promedios se verán feos.

3) Deja de lazy-loadear imágenes por encima del pliegue

Lazy-loading es genial para lo que está bajo el pliegue. Por encima del pliegue, a menudo es auto-sabotaje: le dices al navegador “esto no es importante”, y luego te preguntas por qué LCP sufre.

4) Diseña la estabilidad del layout

Las correcciones de CLS suelen ser aburridas: declara dimensiones, reserva espacio, no inyectes UI que afecte el layout tarde. La ganancia es duradera y no depende del dispositivo, la red o la suerte del usuario.

5) Paga la deuda de main-thread

INP es tu factura de “JavaScript es un impuesto”. La pagas en tareas largas, frameworks pesados y scripts de terceros. La solución no es un truco único; es disciplina continua: bundles más pequeños, menos observers, menos churn de re-render, planificación más inteligente.

6) Trata los scripts de terceros como dependencias de producción

Tags de marketing, A/B testing, chat widgets, detección de fraude: se ejecutan en la CPU de tus usuarios, no en la tuya. Pueden dominar INP e incluso dañar LCP si se ejecutan temprano. Cárgalos más tarde, sandboxéalos o elimínalos. Sí, elimínalos.

Broma #2: El script de terceros más rápido es el que tu equipo legal ya aprobó eliminar.

Hechos e historia que explican el lío actual

  • Hecho 1: “DOMContentLoaded” y “onload” se volvieron populares porque eran fáciles de medir, no porque coincidieran con la percepción del usuario.
  • Hecho 2: HTTP/2 cambió el trade-off “un gran bundle vs muchos archivos pequeños” al multiplexar peticiones, pero el bloqueo por cabeza de línea no desapareció por completo; el transporte sigue importando.
  • Hecho 3: El uso masivo de SPAs desplazó las fallas de rendimiento de estar ligadas a la red hacia estar ligadas a la CPU: los usuarios esperan por parse/ejecución de JS, no solo por bytes.
  • Hecho 4: Las fuentes web solían ser una mejora visual sencilla; ahora son un riesgo de rendimiento porque afectan el renderizado y pueden provocar shifts al intercambiarse.
  • Hecho 5: La era de “lazy-load todo” fue reacción a páginas pesadas, pero creó una nueva clase de bugs: posponer exactamente el contenido que los usuarios vinieron a ver.
  • Hecho 6: Google introdujo Web Vitals para impulsar métricas estandarizadas y centradas en el usuario; la industria tenía demasiadas definiciones incompatibles de “rápido”.
  • Hecho 7: INP reemplazó a FID porque optimizar solo la primera interacción permitía que páginas “pasaran” mientras tartamudeaban en el uso real.
  • Hecho 8: La inestabilidad del layout empeoró cuando el ecosistema de ad-tech normalizó la inserción dinámica de contenido; CLS es básicamente la metricación de la ira del usuario.
  • Hecho 9: RUM (monitorización de usuarios reales) se volvió crítica porque las pruebas de laboratorio no pueden modelar cada dispositivo, red, limitación de CPU o ecosistema de extensiones.

Guía rápida de diagnóstico (primero/segundo/tercero)

Esta es la ruta más rápida al cuello de botella cuando una página falla CWV. El objetivo: no intentar abordarlo todo. Encuentra el limitador dominante.

Primero: decide si es LCP, CLS o INP (y en qué páginas)

  • Usa RUM para identificar las URLs/templates que más fallan (no solo promedios).
  • Segmenta por clase de dispositivo (móvil suele decir la verdad primero).
  • Mira p75, no la media. CWV se puntúa por percentiles.

Segundo: para LCP, divide en servidor vs cliente vs bytes

  1. ¿TTFB alto? Arregla caché, latencia del origen, coste del SSR, configuración del edge.
  2. ¿TTFB bien pero LCP alto? Mira CSS que bloquea el render, prioridad/tamaño del hero y preloads.
  3. ¿El elemento LCP es texto? Revisa el comportamiento de carga de fuentes y bloqueo por CSS.
  4. ¿El elemento LCP es una imagen? Arregla formato/tamaño, hints de prioridad, caché y evita decodificación tardía.

Tercero: para INP, encuentra tareas largas y manejadores culpables

  • Usa trazas para identificar tareas largas > 50ms; busca repetidas.
  • Identifica manejadores de eventos pesados (click/input/keydown) y tormentas de re-render.
  • Audita scripts de terceros y timers; mide su tiempo en la main-thread.

Triage de CLS: busca inyecciones tardías y dimensiones faltantes

  • Encuentra las principales fuentes de shift; normalmente son un pequeño conjunto de elementos.
  • Arregla reservando espacio y evitando inserciones que afecten el layout encima del pliegue.

Si no puedes responder “¿cuál es el elemento LCP para este template?” en 10 minutos, no estás haciendo ingeniería de rendimiento. Estás haciendo vibes.

Tareas prácticas: comandos, salidas, decisiones

Estas son tareas aburridas y ejecutables que puedes hacer hoy. Cada una tiene: un comando, salida de ejemplo, qué significa y la decisión a tomar. No necesitas todas cada día; necesitas las adecuadas cuando estás atascado.

Task 1: Measure TTFB and caching at the edge with curl

cr0x@server:~$ curl -s -o /dev/null -D - https://www.example.com/ | egrep -i 'HTTP/|cache-control|age|x-cache|server-timing|vary'
HTTP/2 200
cache-control: public, max-age=0, s-maxage=600
age: 512
x-cache: HIT
server-timing: cdn-cache;desc=HIT, edge;dur=12, origin;dur=0
vary: Accept-Encoding

Qué significa: age: 512 y x-cache: HIT sugieren que la respuesta se sirve desde caché. server-timing indica tiempo de origen nulo.

Decisión: Si ves MISS en tráfico real, arregla las claves de caché (cookies, cabeceras Vary, query params) y TTLs del edge antes de tocar JS.

Task 2: Check time to first byte precisely with curl timings

cr0x@server:~$ curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://www.example.com/
dns=0.012 connect=0.045 tls=0.089 ttfb=0.312 total=0.428

Qué significa: TTFB es 312ms. Total 428ms. La red no es el villano principal aquí.

Decisión: Si TTFB > ~800ms en hits de caché, probablemente tienes latencia en edge/origen o sobrecarga en la generación de HTML. Arregla eso primero.

Task 3: Identify render-blocking resources with Lighthouse CI (headless)

cr0x@server:~$ npx lighthouse https://www.example.com/ --quiet --chrome-flags="--headless" --only-categories=performance --output=json --output-path=./lh.json
...Saved JSON report to ./lh.json...
cr0x@server:~$ jq '.audits["render-blocking-resources"].details.items[] | {url, totalBytes, wastedMs}' lh.json | head
{
  "url": "https://www.example.com/assets/app.css",
  "totalBytes": 184322,
  "wastedMs": 410
}
{
  "url": "https://www.example.com/assets/vendor.js",
  "totalBytes": 912443,
  "wastedMs": 280
}

Qué significa: El CSS es grande y bloqueante; el vendor JS también bloquea (probablemente por scripts síncronos o uso indebido de preload).

Decisión: Inlined CSS crítico, separa CSS no crítico y asegúrate de que el JS esté defer/async apropiadamente. No “minifiques más” y des por terminado el trabajo.

Task 4: Confirm the LCP element and its request chain (trace via Chrome DevTools Protocol)

cr0x@server:~$ npx chrome-har-capturer --url https://www.example.com/ --output ./page.har
Saved HAR to ./page.har
cr0x@server:~$ jq '.log.entries[] | select(.response.content.mimeType|test("image|text/html|text/css")) | {url: .request.url, status: .response.status, size: .response.content.size, wait: .timings.wait}' page.har | head
{
  "url": "https://www.example.com/",
  "status": 200,
  "size": 62310,
  "wait": 180
}
{
  "url": "https://www.example.com/assets/app.css",
  "status": 200,
  "size": 184322,
  "wait": 92
}
{
  "url": "https://www.example.com/images/hero.jpg",
  "status": 200,
  "size": 1452200,
  "wait": 210
}

Qué significa: La imagen hero pesa 1.45MB y espera 210ms antes del primer byte. Es un ancla clásico de LCP.

Decisión: Convierte la hero a AVIF/WebP, redimensiona, sirve variantes responsivas y asegúrate de que se solicite temprano (no lazy-load, considera preload).

Task 5: Inspect cacheability and compression of the hero image

cr0x@server:~$ curl -s -I https://www.example.com/images/hero.jpg | egrep -i 'content-type|content-length|cache-control|etag|accept-ranges|content-encoding'
content-type: image/jpeg
content-length: 1452200
cache-control: public, max-age=3600
etag: "a9d1-5f2c9d3f"
accept-ranges: bytes

Qué significa: Es JPEG, grande y cacheada por una hora. La caché está bien; el encoding no.

Decisión: Envía formatos modernos y dimensiones más pequeñas. La caché no salvará a los visitantes en su primera visita.

Task 6: Verify HTML is not accidentally uncacheable due to cookies/Vary

cr0x@server:~$ curl -s -I https://www.example.com/ | egrep -i 'set-cookie|vary|cache-control'
cache-control: private, no-store
set-cookie: session=...; Path=/; Secure; HttpOnly
vary: Cookie

Qué significa: Has indicado a todas las cachés del mundo que se aparten. Esto es un impuesto de TTFB para cada usuario.

Decisión: Separa contenido personalizado de la shell cacheable. Evita Vary: Cookie en HTML a menos que estés listo para pagarlo.

Task 7: Find long tasks in a local trace captured with Chromium

cr0x@server:~$ chromium --headless --disable-gpu --trace-startup --trace-startup-file=./trace.json https://www.example.com/
[0204/090312.112233:INFO:headless_shell.cc(661)] Written trace file to ./trace.json
cr0x@server:~$ jq '[.. | objects | select(has("dur") and has("name")) | select(.dur > 50000) | {name, dur, cat}] | sort_by(.dur) | reverse | .[0:5]' trace.json
[
  {
    "name": "EvaluateScript",
    "dur": 182334,
    "cat": "devtools.timeline"
  },
  {
    "name": "FunctionCall",
    "dur": 93422,
    "cat": "devtools.timeline"
  }
]

Qué significa: Tienes tareas >50ms, especialmente evaluación de scripts. Esto es territorio INP.

Decisión: Reduce JS enviado/ejecutado temprano. Divide bundles, elimina código muerto y retrasa scripts de terceros no críticos.

Task 8: Quantify JS/CSS bytes by route using build artifacts

cr0x@server:~$ ls -lh dist/assets | egrep '\.js$|\.css$' | head
-rw-r--r-- 1 cr0x cr0x  912K Feb  4 09:01 vendor-9a12c.js
-rw-r--r-- 1 cr0x cr0x  286K Feb  4 09:01 app-1b22f.js
-rw-r--r-- 1 cr0x cr0x  181K Feb  4 09:01 app-4aa2.css

Qué significa: El chunk vendor es enorme. Eso suele correlacionar con tiempo de parse/compile en teléfonos de gama media.

Decisión: Audita dependencias. Si estás enviando tres librerías de fechas y dos gestores de estado, elige una y elimina las demás.

Task 9: Detect unused CSS in a page (quick-and-dirty with coverage in Puppeteer)

cr0x@server:~$ node -e '
const puppeteer=require("puppeteer");
(async()=>{
  const b=await puppeteer.launch({headless:"new"});
  const p=await b.newPage();
  await p.coverage.startCSSCoverage();
  await p.goto("https://www.example.com/",{waitUntil:"networkidle2"});
  const cov=await p.coverage.stopCSSCoverage();
  let used=0,total=0;
  for (const c of cov){ total+=c.text.length; used+=c.ranges.reduce((s,r)=>s+(r.end-r.start),0); }
  console.log(`css_used=${(used/1024).toFixed(1)}KB css_total=${(total/1024).toFixed(1)}KB used_pct=${(used/total*100).toFixed(1)}%`);
  await b.close();
})();'
css_used=28.4KB css_total=412.7KB used_pct=6.9%

Qué significa: Estás enviando un abrigo de invierno a la playa. El 93% del CSS no se usa en esta ruta.

Decisión: Divide CSS por ruta, purga estilos no usados y evita frameworks globales cargados en todas partes “por si acaso”.

Task 10: Verify font loading behavior and whether it risks CLS

cr0x@server:~$ curl -s -I https://www.example.com/assets/fonts/brand.woff2 | egrep -i 'content-type|cache-control|timing-allow-origin'
content-type: font/woff2
cache-control: public, max-age=31536000, immutable
timing-allow-origin: *

Qué significa: La fuente es amigable con la caché y expone datos de timing. Buena higiene.

Decisión: Si CLS sigue alto, revisa la falta de font-display y desajustes de métricas de fallback; considera una pila de fuentes del sistema para texto crítico de UI.

Task 11: Find third-party script bloat in requests

cr0x@server:~$ jq -r '.log.entries[].request.url' page.har | egrep -i 'goog|doubleclick|segment|mixpanel|hotjar|optimizely|datadog|newrelic' | sort | uniq | head
https://cdn.segment.com/analytics.js/v1/...
https://www.googletagmanager.com/gtm.js?id=GTM-...

Qué significa: Tienes dependencias de terceros que pueden ejecutarse temprano y con frecuencia.

Decisión: Muévelas detrás del consentimiento y/o después de LCP. Si el negocio insiste, al menos hazlas no bloqueantes y retrasa la inicialización.

Task 12: Check server-side compression and HTML size

cr0x@server:~$ curl -s -H 'Accept-Encoding: gzip, br' -I https://www.example.com/ | egrep -i 'content-encoding|content-type|content-length'
content-type: text/html; charset=utf-8
content-encoding: br
content-length: 24132

Qué significa: Brotli está activado y el HTML está ~24KB comprimido. Está bien.

Decisión: Si no ves compresión, actívala en edge/origen; si el HTML es enorme, deja de inlinear volcados JSON de estado en la página.

Task 13: Validate that your CDN is serving correct image variants by Accept header

cr0x@server:~$ curl -s -I -H 'Accept: image/avif,image/webp,image/*,*/*;q=0.8' https://www.example.com/images/hero | egrep -i 'content-type|vary|cache-control'
content-type: image/avif
vary: Accept
cache-control: public, max-age=31536000, immutable

Qué significa: Sirves AVIF cuando el cliente lo soporta, y varías correctamente por Accept.

Decisión: Si siempre sirves JPEG/PNG, estás pagando un impuesto de ancho de banda que impacta directamente LCP en móvil.

Task 14: Check for accidental no-cache on static assets

cr0x@server:~$ curl -s -I https://www.example.com/assets/app-1b22f.js | egrep -i 'cache-control|etag'
cache-control: public, max-age=31536000, immutable
etag: "a1b2c3"

Qué significa: Bien: assets fingerprinted cacheados a largo plazo.

Decisión: Si ves no-cache en assets fingerprinted, arréglalo inmediatamente; estás forzando descargas repetidas y quemando INP vía trabajo de parse extra.

Tres micro-historias corporativas desde las trincheras de rendimiento

Micro-historia 1: El incidente causado por una suposición equivocada (“CDN significa rápido”)

Un equipo de app de consumo desplegó una funcionalidad de personalización en su landing. Era discreta: “Bienvenido de nuevo” y algunos ítems recomendados. Ya tenían CDN, así que asumieron que el impacto sería despreciable. El cambio pasó tests unitarios, end-to-end y la prueba sintética en una conexión de oficina rápida.

Dos días después, el informe de CWV para móvil se hundió. Tickets de soporte describían “página en blanco” y “el toque no responde”. Producto asumió que era una regresión de JavaScript. Frontend empezó a recortar tamaño de bundle. Backend perfiló APIs. Todos estaban ocupados; nadie era efectivo.

El SRE de guardia hizo lo aburrido: un curl -I en la homepage y miró las cabeceras. El HTML ahora era Cache-Control: private, no-store, además de Vary: Cookie. El código de personalización tocaba el estado de sesión temprano, lo que provocó que el framework marcara la respuesta entera como no cacheable. El CDN no estaba “lento”; estaba evitado. Cada petición golpeaba el origen, hacía SSR y la larga cola de usuarios móviles pagó el precio completo.

La solución no fue heroica. Separaron la página en una shell cacheable y un pequeño bloque personalizado que se obtiene desde el cliente después del primer paint. También recortaron cookies, porque las cabeceras de petición estaban creciendo lo suficiente como para importar en redes limitadas.

La lección es clara: las suposiciones sobre caché no son arquitectura. Las cabeceras sí lo son. Verifica el comportamiento de caché con peticiones reales, no vibes.

Micro-historia 2: La optimización que salió mal (lazy-loading del hero)

Un dashboard B2B tenía CWV fuerte en desktop pero LCP móvil pobre en las páginas de marketing. El equipo decidió “lazy-loadear más imágenes” porque parecía un win fácil y la web está llena de consejos que suenan escritos por alguien que nunca lanzó un sitio con un banner hero.

Pusieron loading="lazy" en todas las imágenes globalmente mediante un wrapper de componente. Se veía bien en dev local. En staging, las pruebas sintéticas mostraron menos bytes totales al inicio. El cambio se desplegó.

En una semana, LCP empeoró. El elemento LCP era la imagen hero, y el navegador ahora la despriorizó. La petición de imagen comenzó más tarde, se decodificó más tarde y el contenido principal llegó más tarde en términos percibidos por el usuario. La conversión bajó ligeramente, no lo suficiente para disparar alarmas, pero sí para que marketing empezara reuniones de “el sitio se siente lento”.

Cuando finalmente trazaron el problema, fue obvio: la hero se retrasó por diseño. Revirtieron el lazy-loading para imágenes por encima del pliegue, añadieron sources responsivos y usaron preload selectivo. El efecto neto fue menos bytes y un paint más temprano—porque priorizaron los bytes correctos.

La lección: el navegador es bueno priorizando cuando no le mientes. Lazy-load no es una virtud; es una herramienta. Úsala donde corresponde.

Micro-historia 3: La práctica aburrida pero correcta que salvó el día (presupuestos y canaries)

Una fintech tenía una cultura de rendimiento nada glamorosa. Tenían presupuestos por ruta: máximo de bytes JS, máximo de bytes CSS y una regla de “no añadir scripts de terceros sin revisión”. Los ingenieros se quejaban ocasionalmente. Luego lo olvidaron, que es el mayor elogio para un mecanismo de control.

Un trimestre, se introdujo un nuevo vendor de analítica. El snippet del vendor era pequeño, pero cargaba una librería más grande y empezó a hacer trabajo pesado en interacciones de usuario. En staging, nadie lo notó; el tráfico y dispositivos de staging no eran representativos y la app ya era “lo suficientemente rápida” en pruebas de laboratorio.

La pipeline de despliegue corrió un canary en producción con gating de RUM. El canary mostró regresión de INP concentrada en dispositivos Android de gama media y en la ruta de checkout. El rollout se pausó automáticamente. Sin drama, sin culpas. El equipo tuvo un diff claro: “INP sube; nuevo script de terceros se carga antes de la interacción”.

Trabajaron con la integración del vendor: retrasaron la inicialización hasta que la página estuvo idle y limitaron el tracking a rutas donde realmente importaba. También lo pusieron detrás del consentimiento. El canary pasó; el despliegue continuó.

La lección: guardarraíles aburridos ganan a postmortems heroicos. Presupuestos y canaries no son “proceso”. Son un sistema que evita que envíes latencia a los clientes.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: LCP es malo solo en la primera visita

Causa raíz: Los assets cachean bien, pero la imagen hero crítica es demasiado grande o no se sirve en formatos modernos; los visitantes de primera vez pagan el coste completo de descarga/decodificación.

Solución: Usa imágenes responsivas y AVIF/WebP; asegúrate de que la hero se solicite temprano; verifica cabeceras de caché y compresión CDN donde aplique.

2) Síntoma: LCP es malo y TTFB también es alto

Causa raíz: Latencia de origen, lentitud en SSR, bypass de caché por cookies o personalización, o CDN mal configurado.

Solución: Haz HTML cacheable; mueve personalización a edge-includes o fetch cliente después del paint; perfila SSR; arregla claves de caché; reduce Vary y bloat de cookies.

3) Síntoma: CLS se dispara en páginas con anuncios o banners de consentimiento

Causa raíz: Inserción tardía en el DOM por encima del pliegue; slots de anuncios que cambian de tamaño después de cargar; banner que empuja contenido hacia abajo.

Solución: Reserva espacio (contenedores fijos o min-height), renderiza placeholders, evita insertar cambios de layout por encima del pliegue después del primer paint.

4) Síntoma: CLS es pequeño en laboratorio pero alto en RUM

Causa raíz: Variabilidad de usuarios reales: diferentes tamaños de viewport provocan cambios de wrapping; las fuentes se intercambian de forma distinta; widgets de terceros se comportan de modo inconsistente.

Solución: Usa RUM para identificar fuentes de shift; prueba con viewports variados; aplica dimensiones explícitas y fallbacks de fuentes estables; restringe contenedores de terceros.

5) Síntoma: INP es malo en páginas “simples”

Causa raíz: Scripts de terceros o listeners globales que hacen trabajo en cada interacción; hydration pesado; tareas largas del runtime del framework.

Solución: Retrasa la inicialización de terceros; elimina listeners no usados; divide la hydration; mueve trabajo no crítico a callbacks de idle; reduce JS enviado.

6) Síntoma: Reducir tamaño de bundle no mejoró INP

Causa raíz: El problema no es tamaño de descarga; son patrones de ejecución (re-renders, layout thrash, manejadores pesados) o un único camino de interacción costoso.

Solución: Traza interacciones; encuentra tareas largas; arregla tormentas de re-render; reduce trabajo síncrono en handlers; usa batching y memoización con cuidado.

7) Síntoma: LCP empeora tras añadir “preload todo”

Causa raíz: Inversión de prioridad: preloading de demasiados recursos compite con el recurso real de LCP.

Solución: Preload solo los recursos realmente críticos (normalmente una hero, quizá una fuente si hace falta). Valida con prioridad de red en trazas.

8) Síntoma: Móvil es mucho peor que desktop en todas las métricas

Causa raíz: JS ligado a CPU y trabajo de layout pesado; el desktop lo enmascara con fuerza bruta.

Solución: Prueba con throttling de CPU y perfiles de dispositivos de gama media; reduce ejecución de JS; rompe tareas largas; simplifica UI y DOM.

Listas de verificación / plan paso a paso

Plan paso a paso para rescatar LCP (una plantilla a la vez)

  1. Identifica el elemento LCP desde RUM (o traza) para esa plantilla. Si no puedes nombrarlo, para y encuéntralo.
  2. Revisa TTFB en cache hit y miss. Si el hit de caché es lento, arregla edge/origen antes del trabajo frontend.
  3. Audita bytes del hero: formato, dimensiones, compresión y cabeceras de caché.
  4. Asegura prioridad: evita lazy-load por encima del pliegue; preload la hero cuando corresponda; no la ahogues con otros preloads.
  5. Reduce bloqueo de render: inline/split CSS crítico; difiere JS no crítico.
  6. Re-mide con RUM en p75 móvil. Despliega y verifica. No te quedes en “en laboratorio se ve mejor”.

Plan paso a paso para estabilizar CLS

  1. Lista los elementos que más shiftan usando RUM o “Layout Shift Regions” de DevTools.
  2. Reserva espacio para imágenes/iframes/anuncios con width/height o aspect-ratio.
  3. Deja de insertar tarde por encima del pliegue o rindéralos en un slot reservado desde el inicio.
  4. Arregla fuentes: garantiza métricas de fallback sensatas; usa una estrategia de font-display que no cause reflows sorpresa.
  5. Re-prueba en múltiples viewports porque diferencias de wrapping pueden crear “CLS solo en ciertas pantallas”.

Plan paso a paso para mejoras de INP que perduren

  1. Traza una interacción mala (click/input) en un perfil de gama media y encuentra las tareas más largas.
  2. Identifica al propietario: tu código vs tercero vs runtime del framework.
  3. Rompe tareas largas y mueve trabajo fuera del camino crítico de interacción.
  4. Reduce re-renders: evita actualizaciones de estado que disparen árboles de componentes grandes; virtualiza listas grandes.
  5. Difiere scripts no críticos y deja de hacer trabajo de analytics síncrono en eventos de input.
  6. Pon un presupuesto en tareas largas y en tamaño de bundle por ruta, y haz que se aplique en CI/canary.

Checklist operacional: mantener las ganancias y evitar regresiones

  • Dashboards RUM por plantilla y clase de dispositivo, con alertas en regresiones p75.
  • Presupuestos de rendimiento para bytes JS/CSS y adiciones de terceros.
  • Rollouts canary con rollback/pausa automática si CWV se degrada.
  • Poda regular de dependencias (trimestral está bien; semanal es fantasía).
  • Tests de cabeceras de caché en CI para rutas y assets críticos.

Preguntas frecuentes

1) ¿Debo optimizar puntuaciones de laboratorio o RUM?

RUM para la verdad, laboratorio para depurar. Las pruebas de laboratorio son controladas; son excelentes para detectar regresiones obvias y aislar causas. RUM te dice lo que realmente experimentan los clientes, incluyendo dispositivos lentos y redes extrañas.

2) ¿Por qué cambia mi puntuación de Lighthouse en cada ejecución?

Porque el rendimiento es un sistema distribuido: jitter de red, scheduling de CPU, estado de caché y comportamiento de terceros varían. Usa múltiples ejecuciones, mira distribuciones y céntrate en cuellos de botella repetibles como héroes enormes, CSS bloqueante y tareas largas.

3) ¿Es una SPA inherentemente peor para CWV?

No inherentemente, pero es más fácil equivocarse. Las SPAs suelen retrasar contenido significativo hasta que JS se ejecuta, lo que perjudica LCP y puede afectar INP. SSR/streaming e hidratación selectiva pueden cerrar la brecha, pero solo si mantienes la ruta crítica ligera.

4) ¿Necesito inlinear todo el CSS?

No. Inlinea el CSS crítico para lo que está por encima del pliegue, separa el resto y evita enviar todo el design system en cada ruta. Inlined todo puede inflar el HTML y retrasar el primer byte en conexiones lentas.

5) ¿Los preloads son siempre buenos?

No. Preloadea uno o dos recursos que realmente definan LCP (a menudo una hero y quizá una fuente). Sobre-preloadear compite por ancho de banda y puede retrasar el recurso crítico real.

6) ¿Cómo afectan las fuentes a CWV?

Las fuentes pueden retrasar el renderizado del texto (perjudicando la carga percibida) y pueden causar shifts al intercambiarse (perjudicando CLS). Cachea fuentes agresivamente, limita variantes y asegura que las métricas de fallback no provoquen grandes reflows.

7) ¿Cuál es el “gran gancho” más rápido para CLS?

Dar dimensiones a todo. Imágenes, iframes, slots de anuncios. Reservar espacio para banners. Las correcciones rápidas de CLS parecen tareas de mantenimiento porque lo son.

8) ¿Cuál es el “gran gancho” más rápido para INP?

Eliminar o retrasar scripts de terceros que se ejecutan en interacción y romper tareas largas. También: dejar de hacer trabajo pesado de forma síncrona en handlers de input. Si debes hacer analytics, búferalos.

9) ¿Por qué móvil se ve desproporcionadamente peor?

Porque CPUs y radios móviles son más lentos, y la presión de memoria lo cambia todo. Desktop oculta muchos pecados. Si solo pruebas en un portátil de desarrollo, estás haciendo benchmark en la máquina equivocada.

10) Si mi backend es rápido, ¿puedo ignorar TTFB?

No. TTFB incluye red, TLS, comportamiento del CDN, enrutamiento en el edge y misses de caché. La latencia del backend puede estar bien mientras los usuarios aún esperan porque accidentalmente hiciste el HTML no cacheable o enroutaste mal el tráfico.

Siguientes pasos que puedes desplegar esta semana

No empieces con “optimiza todo”. Empieza con una plantilla fallida y consigue una ganancia medible en p75 móvil.

  1. Elige la plantilla de mayor tráfico que peor rinda desde RUM (no la homepage por tradición).
  2. Ejecuta la guía rápida de diagnóstico e identifica si TTFB, bloqueo de render, bytes del hero o tareas largas dominan.
  3. Despliega una corrección de LCP: haz el HTML cacheable, prioriza la hero o reduce bytes del hero. Verifica mediante cabeceras y RUM.
  4. Despliega una corrección de CLS: reserva espacio para la principal fuente de shift. Verifica que CLS baje en RUM.
  5. Despliega una corrección de INP: elimina/retrasa un script de terceros o rompe una ruta de tarea larga. Verifica que la latencia de interacción mejore.
  6. Añade guardarraíles: presupuestos, gating con canary y una revisión semanal de “qué añadimos” en dependencias.

El secreto honesto: el mejor trabajo en CWV se parece al trabajo de fiabilidad. Mide, aísla, arregla, verifica y previene regresiones. Si haces trucos ingeniosos sin una curva antes/después, no estás afinando rendimiento—estás coleccionando amuletos.

← Anterior
Deriva de tiempo en WSL2: corrige el desfase del reloj correctamente
Siguiente →
Ataques DMA 101: Cómo la IOMMU impide que un dispositivo PCIe controle tu RAM

Deja un comentario