El descubrimiento temprano del recurso
Por qué una imagen declarada en CSS se descubre dos viajes de red tarde, la regla de comparación con el primer recurso, y los cuatro arreglos ordenados de mejor a peor.
El retraso de carga del recurso debería ocupar menos del diez por ciento del LCP y en la práctica es la subparte más inflada de la web, porque su causa no es lenta ni pesada: es una indirección. El navegador no puede pedir lo que no sabe que existe, y hay cuatro formas muy comunes de esconderle la imagen que el usuario está esperando.
- Aplicar la regla de comparación con el primer recurso para saber si hay margen.
- Contar los viajes de red que cuesta una imagen declarada en CSS frente a una en HTML.
- Enumerar qué hace descubrible un recurso y qué lo esconde.
- Ordenar los cuatro arreglos por preferencia y saber por qué ese orden.
La regla del primer recurso
La guía oficial da un criterio que se comprueba en diez segundos y que no exige interpretar nada:
El recurso del elemento LCP debería empezar a cargarse al mismo tiempo que el primer recurso que carga la página. Si empieza más tarde, hay margen de mejora.
La intuición es directa. En cuanto llega el HTML, el navegador arranca las primeras peticiones: la hoja de estilos, el script del framework, lo que haya arriba. Si tu imagen crítica no está entre ellas, algo la está reteniendo.
// Comparacion directa: cuando arranco el primero y cuando el tuyo.
const recursos = performance.getEntriesByType('resource')
.sort((a, b) => a.startTime - b.startTime);
const primero = recursos[0];
const lcp = recursos.find((r) => r.name.includes('hero')); // ajusta el filtro
console.log('primer recurso ', Math.round(primero.startTime) + 'ms', primero.name.split('/').pop());
console.log('recurso del LCP ', Math.round(lcp.startTime) + 'ms', lcp.name.split('/').pop());
console.log('retraso evitable', Math.round(lcp.startTime - primero.startTime) + 'ms');
La guía añade un matiz que evita un falso positivo cómodo: es posible construir una página en la que ningún recurso se descubra pronto y todos arranquen tarde por igual. En ese caso la comparación sale bien y la página está mal. Por eso el número que hay que vigilar de verdad es el retraso respecto al primer byte del documento, no solo respecto al primer recurso.
Lo que cuesta una indirección, en viajes
Dos versiones del mismo hero, con 150 ms de ida y vuelta y un CSS de 40 KB.
Versión en HTML. El escáner de precarga lee la etiqueta de imagen mientras el analizador principal todavía va por la cabecera, y lanza la petición. La imagen empieza a descargarse a la vez que el CSS.
Versión en CSS. La cadena es: pedir el HTML, encontrar la referencia a la hoja de estilos, pedirla, esperar a que llegue entera, parsearla, encontrar la regla de fondo, resolver a qué elemento aplica, y entonces pedir la imagen. El navegador no descarga una imagen de fondo hasta saber que hay un elemento que la usa, y saberlo exige haber construido ya el árbol de estilos.
Cuenta de servilleta con esos parámetros: descarga y parseo del CSS suman fácilmente 400 ms antes de que la imagen exista para el navegador. Cuatrocientos milisegundos en los que la imagen no ha empezado, y que caen enteros en el tramo que debería estar por debajo del diez por ciento.
La misma aritmética se aplica, con distintas constantes, a las otras tres formas de esconder un recurso.
Lo que el navegador puede descubrir escaneando el HTML:
- El elemento de imagen con su
srco susrcsetpresentes en el marcado inicial. - Una imagen de fondo de CSS si además está declarada con una precarga en el HTML o en una cabecera
Link. - Un bloque de texto cuya fuente web esté declarada con una precarga en el HTML o en una cabecera
Link.
Lo que no puede descubrir:
- Una imagen que un script añade al documento.
- Una imagen con carga diferida implementada por una librería que esconde la URL en un atributo de datos.
- Una imagen de fondo de CSS sin precarga.
- Cualquier cosa que dependa de que se ejecute código antes.
La lista larga y sus motivos están en el escáner de precarga. Aquí lo que importa es la consecuencia sobre esta métrica en concreto: cada indirección añade al menos un viaje completo al retraso de carga.
Los cuatro arreglos, en orden de preferencia
Primero: convertirlo en un elemento de imagen en el HTML inicial. Es el arreglo correcto y el que casi nadie considera porque implica tocar el diseño. Una imagen de fondo decorativa puede seguir en CSS; el hero que define el LCP casi siempre puede ser un elemento de imagen posicionado con CSS, con el mismo resultado visual.
<div class="hero">
<img src="/img/hero-1600.avif" alt="" width="1600" height="900"
fetchpriority="high" class="hero__fondo">
<h1>Titular</h1>
</div>
.hero { position: relative; isolation: isolate; }
.hero__fondo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: -1;
}
Con eso el escáner ve la URL, la petición sale con el resto, y encima ganas el control de dimensiones y de prioridad que una imagen de fondo no te da.
Segundo: si tiene que seguir en CSS, precargarla. Con el tipo declarado, para que los navegadores sin soporte del formato no descarguen bytes inútiles, y con prioridad alta, porque una precarga de imagen sale con la prioridad baja de las imágenes:
<link rel="preload" as="image" href="/img/hero-1600.avif"
type="image/avif" fetchpriority="high">
<link rel="stylesheet" href="/css/critico.css">
El orden importa: la precarga antes de la hoja de estilos, para que la petición de la imagen salga en el mismo momento que la del CSS y no después.
Tercero: emitir la pista desde la cabecera HTTP. Si tu servidor tarda en generar el HTML, la etiqueta llega tarde por definición. La cabecera Link, y mejor todavía las pistas tempranas con estado 103, adelantan la descarga al periodo en que el backend todavía está trabajando. Lo vimos en preload.
Cuarto: renderizar el marcado en el servidor. Si tu página se construye en el cliente, el HTML que llega no contiene ninguna referencia y no hay nada que escanear. El renderizado en servidor no solo pinta antes: hace descubribles los recursos, que es la mitad grande del efecto.
El atributo de carga diferida sobre la imagen del hero es la forma más rápida que existe de arruinar esta métrica, y es sorprendentemente común porque alguien aplicó la optimización a todas las imágenes de una plantilla de golpe. Con carga diferida, el navegador no pide la imagen hasta que el layout confirma que está dentro de la ventana, lo que la sitúa por definición después del CSS, del layout y de todo lo que los retrase. Sumas retraso de carga garantizado a cambio de cero. La regla no admite matices: la primera imagen de la ventana inicial nunca lleva carga diferida.
El coste añadido del origen ajeno
Hay un motivo más para que un recurso descubrible arranque tarde, y es que viva en otro dominio. Antes del primer byte de la imagen hay que resolver el nombre, abrir la conexión y negociar el cifrado, que en la red de la cuenta anterior son otros 300 a 450 milisegundos. La anatomía completa está en anatomía de una conexión.
Servir la imagen del elemento principal desde el mismo origen que el documento elimina esa cadena entera y además reutiliza una conexión cuya ventana de congestión ya ha crecido, con lo que la descarga arranca a mayor velocidad. Muchas CDN de imágenes permiten reexponerse bajo tu propio dominio; si la tuya lo permite, es de las intervenciones con mejor relación entre esfuerzo y efecto.
Cuando el dominio ajeno es inevitable, la mitigación es la preconexión, con el modo de credenciales correcto, según lo visto en preconnect y dns-prefetch. Mitiga, no elimina: sigues necesitando que la preconexión se solape con otro trabajo para ganar algo.
// Cuanto de tu retraso de carga es establecimiento de conexion.
const r = performance.getEntriesByType('resource')
.find((x) => x.name.includes('hero'));
console.log('DNS ', Math.round(r.domainLookupEnd - r.domainLookupStart) + 'ms');
console.log('Conexion ', Math.round(r.connectEnd - r.connectStart) + 'ms');
console.log('Espera ', Math.round(r.responseStart - r.requestStart) + 'ms');
console.log('Descarga ', Math.round(r.responseEnd - r.responseStart) + 'ms');
Si las dos primeras líneas suman cientos de milisegundos, el problema del recurso no es su tamaño ni su descubrimiento: es dónde vive.
Todas las herramientas del navegador te enseñan el DOM, es decir, el resultado de haber ejecutado tu JavaScript y aplicado tu CSS. El escáner de precarga no ve nada de eso: ve el flujo de bytes tal como sale del servidor. Esa diferencia hace que una imagen que en el inspector aparece con un src impecable pueda ser completamente invisible para el descubrimiento temprano, porque ese src lo puso un script. La comprobación que no miente es pedir el documento sin ejecutar nada y buscar la URL a mano. Si tu imagen crítica no sale en esa salida, ninguna cantidad de fetchpriority la va a salvar, porque la prioridad decide cómo se descarga algo que ya se ha decidido pedir, no cuándo se entera el navegador de que hay que pedirlo. Y hay un corolario que ahorra discusiones enteras en revisión de código: cualquier framework, plugin o componente que prometa optimizar imágenes moviendo URLs con JavaScript está, por construcción, empeorando el descubrimiento de las imágenes de la primera pantalla, por muy bien que lo haga con las de más abajo. La distinción entre imagen visible al principio y el resto no la puede hacer una librería en tiempo de ejecución sin llegar tarde; la tienes que hacer tú en tiempo de plantilla.
curl -s https://ejemplo.com | grep -o -E '(src|srcset|href)="[^"]*hero[^"]*"'