El escáner de precarga: qué ve y cómo cegarlo
El componente que hace que la web sea usable pese a los scripts bloqueantes, qué patrones lo dejan ciego, y cómo comprobar si tus recursos críticos son visibles para él.
Cuando el parser se detiene en un script síncrono, el navegador no se queda de brazos cruzados: un segundo analizador va por delante leyendo el resto del HTML en bruto y lanzando peticiones para todo lo que encuentre. Ese componente es la razón de que la web con scripts bloqueantes siga siendo tolerable, y también la razón de que ciertos patrones de carga sean desastrosos: le dejan ciego.
- Explicar qué hace el escáner de precarga y por qué existe.
- Enumerar qué recursos ve y cuáles no.
- Reconocer los cinco patrones que lo dejan ciego.
- Comprobar si un recurso crítico es descubrible desde el HTML.
Qué hace y por qué existe
El escáner de precarga es un analizador secundario y ligero que recorre el flujo de HTML en bruto buscando referencias a recursos, sin construir DOM ni ejecutar nada. Su único trabajo es encontrar URLs y lanzar peticiones cuanto antes.
Existe porque el analizador principal se detiene con frecuencia: cada script síncrono lo para, y durante esa pausa la red estaría ociosa. El escáner llena ese hueco. Cuando el parser reanuda y llega a una imagen, es probable que esa imagen ya esté descargada o en camino.
Su efecto es difícil de exagerar. Las mediciones históricas de su introducción reportaron mejoras de carga sustanciales, y hoy es un componente que existe en todos los motores. Buena parte de las técnicas de este track consisten, en el fondo, en no estropear el trabajo que el escáner ya hace gratis.
Qué ve
El escáner lee atributos en el HTML en bruto. Ve, entre otros:
- El atributo
srcy elsrcsetde un elemento de imagen. - El
srcde un script y de un iframe. - El
hrefde una hoja de estilos. - Las declaraciones de precarga y de preconexión.
- Los elementos de fuente dentro de un elemento de imagen adaptativa.
- El póster de un vídeo.
Y no ve nada que requiera ejecutar código o resolver estilos:
- URLs dentro de CSS, incluidas las imágenes de fondo y las fuentes.
- URLs construidas o asignadas por JavaScript.
- URLs guardadas en atributos de datos que una biblioteca lee después.
- Cualquier cosa dentro de una plantilla que se instancie más tarde.
- Recursos referenciados desde dentro de otro recurso.
Esa segunda lista es la lista de formas de cegarlo, y merece desarrollarse una a una.
Los cinco patrones que lo ciegan
Uno: la imagen de fondo en CSS. Es el más común y el más caro cuando afecta al elemento principal de la página.
/* El escaner no ve esta URL: hay que descargar y parsear el CSS primero. */
.hero {
background-image: url('/img/hero.avif');
}
La cadena resultante es: descargar HTML, descubrir CSS, descargar CSS, parsear CSS, descubrir imagen, descargar imagen. Cuatro pasos en serie donde con una etiqueta de imagen habría dos.
La solución, si esa imagen es el elemento principal, es declararla explícitamente en el HTML:
<link rel="preload" as="image" href="/img/hero.avif" type="image/avif" fetchpriority="high">
Dos: la carga diferida implementada por JavaScript. El patrón histórico de guardar la URL en un atributo de datos y que una biblioteca la mueva al atributo real cuando el elemento se acerca a la ventana:
<!-- Invisible para el escaner. -->
<img data-src="/img/foto.avif" class="diferida" alt="">
Para imágenes por debajo del pliegue es aceptable y es justo lo que se quiere. Para la imagen del área visible inicial es catastrófico: obliga a esperar a que cargue y se ejecute la biblioteca. Hoy no hace falta: el atributo nativo de carga diferida hace lo mismo sin cegar al escáner y sin JavaScript.
Tres: el marcado inyectado por JavaScript. Si tu página se renderiza en el cliente, el HTML que llega no contiene ninguna referencia a las imágenes ni a las fuentes: el escáner no tiene nada que escanear. Es una de las razones estructurales por las que el renderizado en cliente puro tiene peor LCP, y por las que el renderizado en servidor lo mejora: no solo pinta antes, sino que hace descubribles los recursos.
Cuatro: la fuente referenciada desde el CSS. Toda fuente web se declara dentro de una regla de CSS, así que ninguna fuente es descubrible por el escáner sin ayuda. Si el texto de tu elemento principal depende de una fuente, esa cadena está en tu ruta crítica y se resuelve con una precarga explícita.
Cinco: la importación de CSS dentro de otro CSS. Ya lo vimos: el escáner no lee dentro del CSS, así que cada nivel de importación es un viaje en serie que no se solapa con nada.
El escáner solo ve atributos de elementos en el HTML que llega por la red. Todo lo que esté detrás de una capa de indirección, sea CSS, JavaScript o una plantilla, es invisible para él. Si un recurso es crítico y está detrás de una indirección, hay que darle una referencia directa en el HTML.
Comprobar si un recurso es descubrible
Tres comprobaciones, de más rápida a más fiable.
Uno: mirar el HTML en bruto. No el DOM inspeccionado, que ya incluye lo que JavaScript ha hecho, sino la respuesta tal como llega:
# El HTML tal como lo recibe el escaner, sin ejecutar nada.
curl -s https://ejemplo.com | grep -o -E '(src|href|srcset)="[^"]*\.(avif|webp|jpg|png|css|js|woff2)"'
Si tu imagen principal no aparece en esa salida, el escáner no la ve.
Dos: mirar el tipo iniciador en los tiempos de recurso. Este campo dice quién provocó la petición:
performance.getEntriesByType('resource')
.filter((r) => r.initiatorType === 'css' || r.initiatorType === 'script')
.map((r) => ({
recurso: r.name.split('/').pop().slice(0, 40),
iniciador: r.initiatorType,
inicio: Math.round(r.startTime),
}))
.forEach((x) => console.log(x));
Un valor de css significa que el recurso se descubrió al parsear una hoja de estilos, es decir, tarde. Un valor de script significa que lo pidió código. Cualquiera de los dos en un recurso crítico es una bandera roja.
Tres: comparar el instante de inicio con el del primer recurso. El criterio publicado en la guía de optimización de LCP es directo: el recurso del elemento principal debería empezar a cargarse al mismo tiempo que el primer recurso de la página. Si empieza claramente después, hay margen de mejora.
const recursos = performance.getEntriesByType('resource')
.sort((a, b) => a.startTime - b.startTime);
const primero = recursos[0];
console.log('primer recurso:', primero.name, Math.round(primero.startTime), 'ms');
// Compara con el instante de inicio del recurso de tu elemento principal.
El escáner no lo arregla todo
Dos matices que evitan conclusiones excesivas.
El escáner descubre, no prioriza. Encontrar una URL pronto no significa que se descargue pronto: el navegador sigue aplicando su esquema de prioridades y su límite de peticiones en vuelo. Un recurso descubierto pronto con prioridad baja puede llegar tarde igualmente.
El escáner no puede adivinar el orden de importancia. Ve las URLs en el orden del documento y las pide en ese orden, con la prioridad que corresponde a su tipo. Si tu imagen principal está en la posición cuarenta del documento, se descubre después de las treinta y nueve anteriores.
De ahí sale una recomendación estructural: el orden del documento importa. No solo por lo que quepa en la ventana inicial de la conexión, sino porque es el orden en que el escáner descubre las cosas. Poner las referencias críticas pronto es gratis y ayuda.
El argumento habitual a favor de renderizar en el servidor es que el usuario ve contenido antes porque el HTML ya viene con el marcado. Es cierto y es la mitad menor del efecto. La mitad mayor es que el HTML renderizado en el servidor contiene las referencias a los recursos, y por tanto el escáner puede empezar a descargar la imagen del área visible mientras el navegador aún está descargando y ejecutando el resto del JavaScript. Con renderizado puro en cliente, el orden es forzosamente secuencial: HTML, JavaScript, ejecución, inserción del marcado, descubrimiento de la imagen, descarga de la imagen. Con renderizado en servidor, la imagen empieza a descargarse en paralelo con la descarga del JavaScript, y como el JavaScript suele ser lo más pesado, esa superposición vale más que el propio adelanto del pintado. Es también la razón por la que un sitio renderizado en cliente que añade una simple etiqueta de precarga de su imagen principal en el HTML estático puede recuperar buena parte de la diferencia sin cambiar de arquitectura, que es de las intervenciones con mejor relación entre coste y efecto de todo el catálogo.