Las cuatro subpartes del LCP
El desglose que convierte mejora el LCP en una tarea concreta: qué mide cada subparte, qué porcentaje debería ocupar en una página sana, y el código para medirlas por separado.
“El LCP está en 3,8 segundos” no es un diagnóstico, es un síntoma. El desglose oficial parte ese número en cuatro tramos que no se solapan y que suman exactamente el total, cada uno con su propia lista de remedios. Con el desglose delante, la pregunta deja de ser qué hacer y pasa a ser cuál de los cuatro tramos está fuera de proporción.
- Definir las cuatro subpartes con precisión y saber que suman el total sin huecos.
- Comparar tu reparto con el de una página bien optimizada usando los porcentajes de referencia.
- Medir las cuatro por separado desde JavaScript con las tres API implicadas.
- Reconocer el desplazamiento de tiempo entre subpartes que simula una mejora.
El desglose
Las cuatro subpartes están definidas en la guía oficial de optimización de la métrica, y su propiedad más importante es estructural: no hay solapamiento ni hueco entre ellas. Suman el LCP completo.
Tiempo hasta el primer byte. Desde que el usuario inicia la carga hasta que llegan los primeros bytes de la respuesta del documento. Todo lo demás ocurre después, así que este tramo es un suelo que ninguna optimización posterior puede recuperar.
Retraso de carga del recurso. Desde el primer byte del documento hasta que el navegador empieza a pedir el recurso del elemento LCP. Es el tiempo que el navegador tarda en enterarse de que ese recurso existe y en decidir pedirlo. Si el elemento no necesita recurso, porque es texto con una fuente ya disponible, vale cero.
Duración de la carga del recurso. Lo que tarda la descarga en sí. También vale cero si no hay recurso.
Retraso de renderizado. Desde que el recurso termina de descargarse hasta que el elemento aparece pintado del todo.
flowchart TB N[Inicio de la navegacion] --> A A[TTFB unos 40 por ciento] --> B[Retraso de carga menos del 10 por ciento] B --> C[Duracion de la carga unos 40 por ciento] C --> D[Retraso de renderizado menos del 10 por ciento] D --> E[Instante del LCP] A --> A2[Servidor CDN y redirecciones] B --> B2[Descubrimiento y prioridad] C --> C2[Tamano formato y distancia] D --> D2[Bloqueo de render y hilo principal] style A fill:#89b4fa,color:#11111b style B fill:#f38ba8,color:#11111b style C fill:#89b4fa,color:#11111b style D fill:#f38ba8,color:#11111b style E fill:#a6e3a1,color:#11111b style A2 fill:#f9e2af,color:#11111b style B2 fill:#f9e2af,color:#11111b style C2 fill:#f9e2af,color:#11111b style D2 fill:#f9e2af,color:#11111b
Los dos tramos en rojo son los que llevan la palabra retraso en el nombre, y eso es una pista deliberada: son tiempo en el que no se está transfiriendo nada útil. Los dos azules involucran transferencia de red y por su naturaleza cuestan tiempo.
Los porcentajes de referencia
El reparto de una página bien optimizada, según la guía oficial:
| Subparte | Porcentaje objetivo |
|---|---|
| Tiempo hasta el primer byte | En torno al 40 % |
| Retraso de carga del recurso | Menos del 10 % |
| Duración de la carga del recurso | En torno al 40 % |
| Retraso de renderizado | Menos del 10 % |
| Total | 100 % |
La idea que hay detrás cabe en dos frases. La inmensa mayoría del tiempo debería estar gastada en descargar el documento y el recurso del elemento principal. Cualquier instante anterior al LCP en el que ninguno de esos dos se esté descargando es una oportunidad de mejora.
Dos advertencias que la propia guía subraya y que conviene no saltarse.
Son orientaciones, no reglas. Si tus páginas están consistentemente por debajo de 2,5 segundos, el reparto relativo da igual. El desglose sirve para diagnosticar, no para puntuar.
No los conviertas en números absolutos. Es tentador multiplicar el umbral de 2,5 segundos por los porcentajes y salir con un presupuesto de un segundo para el TTFB. La guía lo desaconseja explícitamente: estos tramos solo son significativos unos respecto a otros, y hay que medirlos siempre así.
Lo que sí funciona es la lectura por desproporción. Un retraso de carga del 35 por ciento no significa que sobren tantos milisegundos: significa que tienes un problema de descubrimiento, y que hasta que lo arregles no merece la pena tocar nada más.
Medirlas desde JavaScript
Hacen falta tres API a la vez: la del LCP para el instante final, la de navegación para el primer byte, y la de recursos para el tramo de descarga.
function desglosarLCP(entrada) {
const nav = performance.getEntriesByType('navigation')[0];
const t0 = nav?.activationStart || 0;
const lcp = Math.max(0, entrada.startTime - t0);
const ttfb = Math.max(0, (nav?.responseStart || 0) - t0);
// El recurso del elemento LCP, si lo hay.
const recurso = entrada.url
? performance.getEntriesByType('resource').find((r) => r.name === entrada.url)
: null;
// Sin recurso, los dos tramos centrales valen cero y el tiempo
// restante es todo retraso de renderizado.
const inicio = recurso ? Math.max(0, (recurso.requestStart || recurso.startTime) - t0) : ttfb;
const fin = recurso ? Math.max(0, recurso.responseEnd - t0) : ttfb;
return {
lcp: Math.round(lcp),
ttfb: Math.round(ttfb),
retrasoCarga: Math.round(Math.max(0, inicio - ttfb)),
duracionCarga: Math.round(Math.max(0, fin - inicio)),
retrasoRender: Math.round(Math.max(0, lcp - fin)),
};
}
Y la lectura en porcentajes, que es como hay que mirarlo:
function imprimirDesglose(d) {
const filas = [
['TTFB', d.ttfb, 40],
['Retraso de carga', d.retrasoCarga, 10],
['Duracion de carga', d.duracionCarga, 40],
['Retraso de render', d.retrasoRender, 10],
];
console.log('LCP total:', d.lcp, 'ms');
for (const [nombre, ms, objetivo] of filas) {
const pct = Math.round((ms / d.lcp) * 100);
console.log(
(pct > objetivo ? '[!]' : '[ ]'),
nombre.padEnd(20),
String(ms).padStart(5) + 'ms',
String(pct).padStart(3) + '%',
'| referencia ' + objetivo + '%',
);
}
}
Enganchado al observador de la lección anterior:
let ultima = null;
new PerformanceObserver((l) => {
const e = l.getEntries();
ultima = e[e.length - 1];
}).observe({ type: 'largest-contentful-paint', buffered: true });
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && ultima) {
imprimirDesglose(desglosarLCP(ultima));
}
}, { once: true });
Los campos requestStart y responseEnd de un recurso de otro origen valen cero si la respuesta no incluye la cabecera Timing-Allow-Origin. Con ceros, el retraso de carga sale negativo, la duración sale absurda y el retraso de renderizado se come el total. No hay error ni excepción: salen números plausibles y equivocados. Si tus imágenes vienen de una CDN o de un dominio de imágenes, esa cabecera es requisito previo para medir nada:
Timing-Allow-Origin: https://ejemplo.comO, para exponerlo a cualquier origen, Timing-Allow-Origin: *. Comprueba siempre que el recurso aparece en performance.getEntriesByType('resource') con tiempos distintos de cero antes de fiarte de un desglose.
El desplazamiento de tiempo que parece una mejora
Este es el motivo por el que el desglose es tan valioso, y merece un ejemplo completo con números.
Página inicial, LCP de 3.000 ms:
| Subparte | Antes |
|---|---|
| TTFB | 800 ms |
| Retraso de carga | 200 ms |
| Duración de la carga | 900 ms |
| Retraso de renderizado | 1.100 ms |
Alguien mira ese perfil, ve 900 ms de descarga, y hace lo razonable: recomprime la imagen y cambia de formato. La descarga baja de 900 a 300 ms, un ahorro de 600 ms perfectamente real y medible en el panel de red.
El LCP sigue en 3.000 ms.
| Subparte | Después |
|---|---|
| TTFB | 800 ms |
| Retraso de carga | 200 ms |
| Duración de la carga | 300 ms |
| Retraso de renderizado | 1.700 ms |
El tiempo no ha desaparecido: se ha mudado. En esta página el elemento estaba oculto hasta que terminaba de ejecutarse un script, así que la imagen podía llegar cuando quisiera; el momento del pintado lo decidía otra cosa. Los 600 ms ahorrados en la descarga se han convertido en 600 ms más de espera en el retraso de renderizado.
La lección práctica es doble. Primero: el cuello de botella era el retraso de renderizado desde el principio, y el desglose lo señalaba con un 37 por ciento donde la referencia es menos del 10. Segundo: optimizar una subparte que no es la desproporcionada no empeora nada, pero tampoco mejora el número, y consume el presupuesto de credibilidad del equipo.
De ahí sale el orden de trabajo que recomienda la guía oficial, que no es el orden cronológico de las subpartes sino el de impacto esperado: primero el retraso de carga, después el retraso de renderizado, después la duración de la carga, y el TTFB al final por ser el que menos control directo tienes sobre él. Los dos “retrasos” van primero porque su objetivo es cero y porque su causa suele ser un fallo estructural con arreglo barato.
Cuando el desglose llega a un panel de métricas, el primer instinto es graficar la media de cada subparte y mirar la tendencia. Es una trampa fina, porque las páginas de un mismo sitio tienen perfiles cualitativamente distintos y promediarlos produce un perfil que no corresponde a ninguna página real. Las fichas de producto suelen estar dominadas por la duración de la carga porque llevan imágenes grandes. La portada suele estar dominada por el TTFB porque se genera bajo demanda y la visitan usuarios nuevos con la caché vacía. Los resultados de búsqueda suelen estar dominados por el retraso de renderizado porque se construyen en cliente. La media de las tres da un perfil equilibrado en el que nada destaca, y la conclusión será que no hay nada roto cuando en realidad hay tres cosas rotas distintas. La forma correcta es segmentar el desglose por plantilla de página y, dentro de cada plantilla, por tipo de elemento LCP, y mirar el percentil 75 de cada subparte por separado en lugar de la media. Con esa segmentación los tres problemas aparecen inmediatamente, cada uno con su remedio, y además descubres algo que la media esconde siempre: que la plantilla con peor LCP casi nunca es la que más tráfico tiene, y que la que más tráfico tiene suele estar a cien milisegundos del umbral, que es donde el mismo esfuerzo compra más usuarios dentro del rango bueno.