wandres.dev
PERFORMANCE II · Las métricas en vivo

El desglose de LCP en sus cuatro subpartes

Descomponer la pintura de contenido mayor en tiempo hasta el primer byte, retraso de descubrimiento, descarga y retraso de render, y qué arregla cada tramo.

⏱ 19 min

Decir que la pintura mayor tarda tres segundos y medio es un dato inútil: no señala nada que se pueda cambiar. Descomponer esos tres segundos y medio en cuatro tramos con causas independientes convierte el mismo número en una instrucción concreta, porque cada tramo tiene un conjunto pequeño y conocido de arreglos. El panel hace ese desglose automáticamente y es probablemente la información más directamente accionable de todo el análisis de carga.

🎯 Al terminar esta lección sabrás
  • Descomponer la métrica en sus cuatro subpartes y calcular el reparto porcentual.
  • Identificar qué tramo domina y aplicar el conjunto de arreglos que le corresponde.
  • Distinguir un problema de descubrimiento de uno de descarga, que se confunden constantemente.
  • Calcular el mismo desglose desde el código para medir en campo.

Los cuatro tramos

flowchart LR
a[Navegacion] -->|Tiempo hasta el primer byte| b[Llega el HTML]
b -->|Retraso de descubrimiento| c[Empieza la peticion del recurso]
c -->|Duracion de la descarga| d[El recurso esta disponible]
d -->|Retraso de render| e[El elemento se pinta]
style a fill:#cba6f7,color:#11111b
style b fill:#89b4fa,color:#11111b
style c fill:#f9e2af,color:#11111b
style d fill:#fab387,color:#11111b
style e fill:#a6e3a1,color:#11111b

Tiempo hasta el primer byte. Desde que empieza la navegación hasta que llega el primer byte del documento. Contiene redirecciones, resolución de nombres, conexión, negociación segura y el tiempo de generación del servidor. Es un techo absoluto: nada puede pintarse antes.

Retraso de descubrimiento del recurso. Desde que llega el HTML hasta que el navegador empieza a pedir el recurso que va a pintar el elemento. Es un tramo de espera pura y es el que más veces domina sin que nadie se dé cuenta.

Duración de la descarga. Lo que tarda el recurso en transferirse. Es el único tramo que depende del tamaño del fichero.

Retraso de render. Desde que el recurso está disponible hasta que el píxel aparece. Es tiempo perdido por el hilo principal ocupado, por la fuente que todavía no llegó, o por código que decide cuándo mostrar el elemento.

Una referencia útil para juzgar el reparto: en una carga sana, los dos tramos de espera —descubrimiento y render— deberían ser una fracción pequeña del total, y el reparto debería estar dominado por el primer byte y por la descarga. Cuando el descubrimiento o el render se llevan una parte importante, hay tiempo que se está regalando sin que nadie transfiera ni calcule nada.

Qué arregla cada tramo

Tramo dominante Causas habituales Arreglos
Primer byte Servidor lento, sin caché, redirecciones, origen lejos Caché en el servidor, CDN, eliminar redirecciones, respuesta en streaming
Descubrimiento Recurso en CSS, insertado por JS, con carga perezosa, tras un import Etiqueta en el HTML, preload, fetchpriority, quitar loading="lazy"
Descarga Fichero grande, formato antiguo, sin dimensionar por dispositivo Formatos modernos, tamaños responsivos, compresión, prioridad
Render Hilo principal bloqueado, CSS bloqueante, fuente pendiente, animación de entrada Reducir JS de arranque, CSS crítico, font-display, quitar la aparición gradual

La confusión más frecuente y más cara es entre descubrimiento y descarga, porque las dos se manifiestan igual: la imagen tarda en aparecer. Son problemas opuestos. Si el problema es de descarga, comprimir la imagen ayuda. Si es de descubrimiento, comprimirla no cambia nada porque el tiempo se está yendo antes de que se pida, y una imagen la mitad de grande simplemente se pedirá igual de tarde.

El caso canónico de descubrimiento tardío es una imagen principal declarada como fondo en CSS. El navegador no puede pedirla hasta que ha descargado y analizado la hoja de estilos, ha calculado los estilos y ha determinado que ese elemento existe y es visible. Eso son fácilmente varios cientos de milisegundos de espera que la misma imagen en una etiqueta del HTML no paga, porque el analizador especulativo la ve mientras el documento todavía está llegando.

⚠️
Cuidado

El atributo de carga perezosa aplicado a la imagen principal es una de las regresiones más comunes y más fáciles de introducir: alguien lo aplica en masa a todas las imágenes del proyecto como buena práctica y empeora la métrica principal. La carga perezosa es correcta para todo lo que está por debajo del pliegue e incorrecta para lo que se ve al entrar.

Medir el desglose desde el código

El panel calcula estos cuatro tramos, pero conviene poder calcularlos también en campo, donde no hay panel. Este fragmento los deriva de la entrada de navegación, la entrada de la pintura mayor y la entrada de recurso correspondiente.

// Desglose de la pintura de contenido mayor en sus cuatro subpartes
(() => {
  new PerformanceObserver(lista => {
    const lcp = lista.getEntries().at(-1);
    const nav = performance.getEntriesByType('navigation')[0];
    const ttfb = nav ? nav.responseStart : 0;

    let descubrimiento = 0, descarga = 0;
    if (lcp.url) {
      const rec = performance.getEntriesByType('resource').find(r => r.name === lcp.url);
      if (rec) {
        descubrimiento = Math.max(0, rec.requestStart - ttfb);
        descarga = Math.max(0, rec.responseEnd - rec.requestStart);
      }
    }
    const finRecurso = ttfb + descubrimiento + descarga;
    const render = Math.max(0, lcp.startTime - finRecurso);

    const tabla = [
      { tramo: '1 primer byte', ms: Math.round(ttfb) },
      { tramo: '2 descubrimiento', ms: Math.round(descubrimiento) },
      { tramo: '3 descarga', ms: Math.round(descarga) },
      { tramo: '4 render', ms: Math.round(render) }
    ].map(f => ({ ...f, porcentaje: Math.round(100 * f.ms / lcp.startTime) + '%' }));

    console.log('LCP total:', Math.round(lcp.startTime), 'ms');
    console.log('Elemento:', lcp.element);
    console.table(tabla);
    const peor = tabla.slice().sort((a, b) => b.ms - a.ms)[0];
    console.log('Tramo dominante:', peor.tramo, '->', peor.porcentaje);
  }).observe({ type: 'largest-contentful-paint', buffered: true });
})();

Cuando el elemento es un bloque de texto en lugar de una imagen, los tramos dos y tres valen cero y todo el tiempo se reparte entre el primer byte y el render. Eso no es un fallo del cálculo: un texto no tiene recurso propio que descubrir ni descargar, así que su retraso es siempre de servidor o de bloqueo del hilo. En ese caso el sospechoso número uno es la fuente web, que hace que el texto exista pero no se pinte.

El orden de intervención

Los cuatro tramos no se optimizan en el orden en que aparecen sino en orden de relación entre coste y beneficio, y ese orden es bastante estable entre proyectos.

Primero, el descubrimiento. Es el tramo con el mejor retorno: mover una etiqueta al HTML o añadir una precarga son cambios de una línea que pueden eliminar cientos de milisegundos. Y es tiempo puro de espera, así que la ganancia es limpia.

Segundo, el render. Casi siempre significa reducir el JavaScript que se ejecuta durante el arranque, y eso mejora las tres métricas a la vez. Cuesta más trabajo pero es la inversión más rentable a medio plazo.

Tercero, la descarga. Formatos modernos y tamaños adecuados. Es trabajo mecánico, con ganancia predecible y sin riesgo.

Cuarto, el primer byte. Suele ser el tramo más difícil porque depende de infraestructura y a veces de otro equipo. Vale la pena cuando es claramente dominante, y en ese caso la ganancia beneficia a todas las páginas del sitio a la vez.

La subparte que casi siempre domina es la que no aparece en ninguna recomendación automática

Si comparas muchos desgloses reales, aparece un patrón que contradice el consejo estándar sobre imágenes: el tramo que más veces domina no es la descarga, sino el descubrimiento, y el descubrimiento es precisamente el tramo del que ninguna herramienta de compresión de imágenes te va a hablar. La razón es estructural y tiene que ver con cómo se construyen las aplicaciones modernas. El navegador tiene un mecanismo excelente para empezar a pedir recursos antes de haber terminado de procesar el documento: un analizador especulativo que recorre el HTML por delante y dispara peticiones de todo lo que reconoce. Ese mecanismo es rapidísimo y es gratis, y cualquier cosa que oculte una URL al analizador lo desactiva. Ocultan URLs: declararlas en CSS en lugar de en HTML, insertarlas con JavaScript, calcularlas en tiempo de ejecución, servirlas desde un componente que solo se monta tras la hidratación, cargarlas dentro de un módulo importado dinámicamente, o meterlas en un carrusel que decide cuál mostrar. Es decir: prácticamente todo lo que hace un componente de interfaz normal. La consecuencia es que una aplicación renderizada en el cliente empieza a pedir su imagen principal después de descargar el paquete de JavaScript, analizarlo, ejecutarlo, hidratar el árbol y montar el componente, lo que puede ser dos segundos después de que el HTML estuviera disponible. Ninguna optimización de la imagen recupera esos dos segundos. Y hay un corolario incómodo: el remedio habitual, precargar el recurso, es un parche que funciona y que además es frágil, porque la URL de la precarga se escribe a mano en el documento y la de la aplicación se calcula, así que en cuanto una de las dos cambia, precargas un recurso que ya nadie usa y sigues descubriendo tarde el que sí. El arreglo estructural es que el servidor emita la etiqueta de la imagen principal en el HTML inicial, con sus dimensiones y su prioridad, y que el componente que se hidrata después la adopte en vez de crearla. Es más trabajo que añadir una línea de precarga, y es la diferencia entre arreglar el problema y esconderlo.