Descubre qué es el escáner de precarga del navegador, cómo ayuda al rendimiento y cómo puedes mantenerte fuera de su camino.
Un aspecto que se suele pasar por alto cuando se optimiza la velocidad de la página es conocer un poco sobre los componentes internos del navegador. Los navegadores realizan ciertas optimizaciones para mejorar el rendimiento de formas que nosotros, como desarrolladores, no podemos, pero solo siempre que esas optimizaciones no se vean frustradas de forma involuntaria.
Una optimización interna del navegador que debes comprender es el escáner de precarga del navegador. En esta publicación, se explicará cómo funciona el escáner de precarga y, lo que es más importante, cómo puedes evitar que se interponga en su camino.
¿Qué es un escáner de precarga?
Cada navegador tiene un analizador de HTML principal que tokeniza el lenguaje de marcado sin procesar y lo procesa en un modelo de objetos. Todo esto continúa alegremente hasta que el analizador se detiene cuando encuentra un recurso de bloqueo, como una hoja de estilo cargada con un elemento <link> o una secuencia de comandos cargada con un elemento <script> sin un atributo async o defer.
<link> para un archivo CSS externo, lo que impide que el navegador analice el resto del documento, o incluso que renderice cualquier parte de él, hasta que se descargue y analice el CSS.
En el caso de los archivos CSS, la renderización se bloquea para evitar un destello de contenido sin diseño (FOUC), que es cuando se puede ver brevemente una versión sin diseño de una página antes de que se le apliquen los estilos.
El navegador también bloquea el análisis y la renderización de la página cuando encuentra elementos <script> sin un atributo defer o async.
Esto se debe a que el navegador no puede saber con certeza si una secuencia de comandos determinada modificará el DOM mientras el analizador de HTML principal aún está haciendo su trabajo. Por este motivo, es una práctica común cargar el código JavaScript al final del documento para que los efectos del bloqueo del análisis y la renderización sean marginales.
Estas son buenas razones por las que el navegador debería bloquear el análisis y la renderización. Sin embargo, no es recomendable bloquear ninguno de estos pasos importantes, ya que pueden retrasar el proceso al demorar el descubrimiento de otros recursos importantes. Afortunadamente, los navegadores hacen todo lo posible para mitigar estos problemas a través de un analizador de HTML secundario llamado analizador de precarga.
<body>, pero el escáner de precarga puede mirar hacia adelante en el lenguaje de marcado sin procesar para encontrar ese recurso de imagen y comenzar a cargarlo antes de que se desbloquee el analizador de HTML principal.
La función de un escáner de precarga es especulativa, lo que significa que examina el lenguaje de marcado sin procesar para encontrar recursos que se recuperen de forma oportuna antes de que el analizador de HTML principal los descubra.
Cómo saber cuándo funciona el escáner de precarga
El escáner de precarga existe debido al bloqueo de la renderización y el análisis. Si estos dos problemas de rendimiento nunca existieran, el escáner de precarga no sería muy útil. La clave para determinar si una página web se beneficia del escáner de precarga depende de estos fenómenos de bloqueo. Para ello, puedes introducir un retraso artificial en las solicitudes para averiguar dónde está funcionando el escáner de precarga.
Tomemos como ejemplo esta página de texto e imágenes básicos con una hoja de estilo. Debido a que los archivos CSS bloquean la renderización y el análisis, se introduce un retraso artificial de dos segundos para la hoja de estilo a través de un servicio de proxy. Este retraso facilita ver en la cascada de red dónde está funcionando el escáner de precarga.
Como puedes ver en la cascada, el escáner de precarga descubre el elemento <img> incluso mientras se bloquean la renderización y el análisis del documento. Sin esta optimización, el navegador no puede recuperar elementos de forma oportunista durante el período de bloqueo, y más solicitudes de recursos serían consecutivas en lugar de simultáneas.
Con ese ejemplo de juguete fuera del camino, veamos algunos patrones del mundo real en los que se puede derrotar al escáner de precarga y qué se puede hacer para corregirlos.
Se insertaron async secuencias de comandos
Supongamos que tienes HTML en tu <head> que incluye un poco de JavaScript intercalado como este:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
Las secuencias de comandos insertadas son async de forma predeterminada, por lo que, cuando se inserte esta secuencia de comandos, se comportará como si se le hubiera aplicado el atributo async. Esto significa que se ejecutará lo antes posible y no bloqueará la renderización. Suena óptimo, ¿verdad? Sin embargo, si supones que este <script> intercalado viene después de un elemento <link> que carga un archivo CSS externo, obtendrás un resultado subóptimo:
async insertada. El escáner de precarga no puede descubrir la secuencia de comandos durante la fase de bloqueo de renderización, ya que se inserta en el cliente.
Analicemos lo que sucedió aquí:
- A los 0 segundos, se solicita el documento principal.
- A los 1.4 segundos, llega el primer byte de la solicitud de navegación.
- A los 2.0 segundos, se solicitan el CSS y la imagen.
- Debido a que el analizador está bloqueado cargando la hoja de estilo y el JavaScript intercalado que inserta la secuencia de comandos
asyncviene después de esa hoja de estilo a los 2.6 segundos, la funcionalidad que proporciona esa secuencia de comandos no está disponible tan pronto como podría estarlo.
Esto no es óptimo porque la solicitud de la secuencia de comandos se produce solo después de que se termina de descargar la hoja de estilo. Esto retrasa la ejecución de la secuencia de comandos lo antes posible. Por el contrario, debido a que el elemento <img> es detectable en el lenguaje de marcado proporcionado por el servidor, el escáner de precarga lo descubre.
Entonces, ¿qué sucede si usas una etiqueta <script> normal con el atributo async en lugar de insertar la secuencia de comandos en el DOM?
<script src="/yall.min.js" async></script>
Este es el resultado:
async <script>. El escáner de precarga descubre la secuencia de comandos durante la fase de bloqueo de renderización y la carga de forma simultánea con el CSS.
Puede haber cierta tentación de sugerir que estos problemas se podrían solucionar con rel=preload. Sin duda, esto funcionaría, pero podría tener algunos efectos secundarios. Después de todo, ¿por qué usar rel=preload para solucionar un problema que se puede evitar no insertando un elemento <script> en el DOM?
async insertada, pero la secuencia de comandos async se precarga para garantizar que se descubra antes.
La carga previa "soluciona" el problema aquí, pero presenta un problema nuevo: la secuencia de comandos async en las dos primeras demostraciones, a pesar de que se carga en el <head>, se carga con prioridad "Baja", mientras que la hoja de estilo se carga con prioridad "Más alta". En la última demostración, en la que se carga previamente la secuencia de comandos async, la hoja de estilo se sigue cargando con la prioridad "Más alta", pero la prioridad de la secuencia de comandos se ascendió a "Alta".
Cuando se aumenta la prioridad de un recurso, el navegador le asigna más ancho de banda. Esto significa que, aunque la hoja de estilo tenga la prioridad más alta, la prioridad elevada de la secuencia de comandos puede causar una contención de ancho de banda. Esto podría ser un factor en las conexiones lentas o en los casos en que los recursos son bastante grandes.
La respuesta aquí es sencilla: si se necesita una secuencia de comandos durante el inicio, no anules el escáner de precarga insertándola en el DOM. Experimenta según sea necesario con la posición del elemento <script>, así como con atributos como defer y async.
Carga diferida con JavaScript
La carga diferida es un excelente método para conservar datos, que suele aplicarse a las imágenes. Sin embargo, a veces, la carga diferida se aplica de forma incorrecta a las imágenes que están "en la mitad superior de la página", por así decirlo.
Esto genera posibles problemas con la visibilidad de los recursos en lo que respecta al escáner de precarga y puede retrasar innecesariamente el tiempo que se tarda en descubrir una referencia a una imagen, descargarla, decodificarla y presentarla. Tomemos como ejemplo este lenguaje de marcado de imagen:
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
El uso de un prefijo data- es un patrón común en los cargadores diferidos con tecnología de JavaScript. Cuando la imagen se desplaza hacia el viewport, el cargador diferido quita el prefijo data-, lo que significa que, en el ejemplo anterior, data-src se convierte en src. Esta actualización le indica al navegador que recupere el recurso.
Este patrón no es problemático hasta que se aplica a las imágenes que están en el viewport durante el inicio. Debido a que el escáner de precarga no lee el atributo data-src de la misma manera que lo haría con un atributo src (o srcset), la referencia de la imagen no se descubre antes. Peor aún, la imagen se retrasa hasta después de que se descarga, compila y ejecuta el JavaScript del cargador diferido.
Según el tamaño de la imagen, que puede depender del tamaño del viewport, puede ser un elemento candidato para el Largest Contentful Paint (LCP). Cuando el escáner de precarga no puede recuperar de forma especulativa el recurso de imagen con anticipación, posiblemente durante el momento en que las hojas de estilo de la página bloquean la renderización, el LCP se ve afectado.
La solución es cambiar el lenguaje de marcado de la imagen:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
Este es el patrón óptimo para las imágenes que están en el viewport durante el inicio, ya que el escáner de precarga descubrirá y recuperará el recurso de imagen con mayor rapidez.
El resultado en este ejemplo simplificado es una mejora de 100 milisegundos en el LCP en una conexión lenta. Esto puede no parecer una gran mejora, pero lo es cuando consideras que la solución es una corrección rápida del lenguaje de marcado y que la mayoría de las páginas web son más complejas que este conjunto de ejemplos. Esto significa que los candidatos de LCP pueden tener que competir por el ancho de banda con muchos otros recursos, por lo que las optimizaciones como esta se vuelven cada vez más importantes.
Imágenes de fondo de CSS
Recuerda que el escáner de precarga del navegador analiza el lenguaje de marcado. No analiza otros tipos de recursos, como CSS, que pueden implicar recuperaciones de imágenes a las que hace referencia la propiedad background-image.
Al igual que con HTML, los navegadores procesan CSS en su propio modelo de objetos, conocido como CSSOM. Si se descubren recursos externos a medida que se construye el CSSOM, esos recursos se solicitan en el momento del descubrimiento, y no por el escáner de precarga.
Supongamos que el candidato a LCP de tu página es un elemento con una propiedad background-image de CSS. Esto es lo que sucede cuando se cargan los recursos:
background-image de CSS (fila 3). La imagen que solicita no comienza a recuperarse hasta que el analizador de CSS la encuentra.
En este caso, el escáner de precarga no se ve tan afectado, ya que no participa. Aun así, si un candidato de LCP en la página proviene de una propiedad CSS background-image, te recomendamos precargar esa imagen:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
Esa sugerencia de rel=preload es pequeña, pero ayuda al navegador a descubrir la imagen antes de lo que lo haría de otra manera:
background-image de CSS (fila 3). La sugerencia rel=preload ayuda al navegador a descubrir la imagen unos 250 milisegundos antes que sin la sugerencia.
Con la sugerencia rel=preload, el candidato de LCP se descubre antes, lo que reduce el tiempo de LCP. Si bien esa sugerencia ayuda a solucionar este problema, la mejor opción puede ser evaluar si tu imagen candidata para LCP debe cargarse desde CSS o no. Con una etiqueta <img>, tendrás más control sobre la carga de una imagen que sea adecuada para el viewport y, al mismo tiempo, permitirás que el escáner de precarga la descubra.
Inclusión de demasiados recursos intercalados
La inserción de recursos intercalados es una práctica que coloca un recurso dentro del HTML. Puedes intercalar hojas de estilo en elementos <style>, secuencias de comandos en elementos <script> y prácticamente cualquier otro recurso con la codificación en Base64.
La inserción de recursos puede ser más rápida que su descarga, ya que no se emite una solicitud independiente para el recurso. Está en el documento y se carga de forma instantánea. Sin embargo, existen desventajas significativas:
- Si no almacenas en caché tu HTML (y no puedes hacerlo si la respuesta HTML es dinámica), los recursos intercalados nunca se almacenan en caché. Esto afecta el rendimiento porque los recursos intercalados no se pueden reutilizar.
- Incluso si puedes almacenar en caché el HTML, los recursos intercalados no se comparten entre documentos. Esto reduce la eficiencia del almacenamiento en caché en comparación con los archivos externos que se pueden almacenar en caché y reutilizar en todo un origen.
- Si intercalas demasiado, retrasas el escáner de precarga para que descubra recursos más adelante en el documento, ya que la descarga de ese contenido adicional intercalado lleva más tiempo.
Toma esta página como ejemplo. En ciertas condiciones, el candidato de LCP es la imagen en la parte superior de la página, y el CSS se encuentra en un archivo separado que carga un elemento <link>. La página también usa cuatro fuentes web que se solicitan como archivos independientes del recurso CSS.
<img>, pero el escáner de precarga la descubre porque el CSS y las fuentes necesarias para la página se cargan en recursos independientes, lo que no retrasa el trabajo del escáner de precarga.
Ahora, ¿qué sucede si el CSS y todas las fuentes se intercalan como recursos base64?
<img>, pero la inserción del CSS y sus cuatro recursos de fuentes en `` retrasa el escáner de precarga para descubrir la imagen hasta que esos recursos se descarguen por completo.
El impacto de la inserción de código intercalado tiene consecuencias negativas para el LCP en este ejemplo y para el rendimiento en general. La versión de la página que no incluye nada en línea renderiza la imagen del LCP en aproximadamente 3.5 segundos. La página que une todo no renderiza la imagen de LCP hasta que pasan poco más de 7 segundos.
Aquí hay más factores en juego que solo el escáner de precarga. La incorporación de fuentes no es una buena estrategia porque base64 es un formato ineficiente para los recursos binarios. Otro factor que influye es que los recursos de fuentes externas no se descargan, a menos que el CSSOM determine que son necesarios. Cuando esas fuentes se intercalan como base64, se descargan independientemente de si se necesitan para la página actual o no.
¿Podría una precarga mejorar las cosas aquí? Claro. Podrías precargar la imagen del LCP y reducir el tiempo de LCP, pero inflar tu HTML potencialmente no almacenable en caché con recursos intercalados tiene otras consecuencias negativas en el rendimiento. El Primer procesamiento de imagen con contenido (FCP) también se ve afectado por este patrón. En la versión de la página en la que no hay nada intercalado, el FCP es de aproximadamente 2.7 segundos. En la versión en la que todo está intercalado, el FCP es de aproximadamente 5.8 segundos.
Ten mucho cuidado cuando insertes elementos en el código HTML, en especial los recursos codificados en Base64. En general, no se recomienda, excepto para recursos muy pequeños. Usa la menor cantidad posible de funciones intercaladas, ya que usar demasiadas es jugar con fuego.
Cómo renderizar lenguaje de marcado con JavaScript del cliente
No hay dudas de que JavaScript afecta la velocidad de la página. Los desarrolladores no solo dependen de él para proporcionar interactividad, sino que también existe una tendencia a depender de él para entregar el contenido en sí. Esto mejora la experiencia de los desarrolladores en algunos aspectos, pero los beneficios para los desarrolladores no siempre se traducen en beneficios para los usuarios.
Un patrón que puede anular el escáner de precarga es renderizar el lenguaje de marcado con JavaScript del cliente:
Cuando las cargas útiles de lenguaje de marcado están contenidas en JavaScript y se renderizan por completo en el navegador, los recursos de ese lenguaje de marcado son invisibles para el escáner de precarga. Esto retrasa el descubrimiento de recursos importantes, lo que sin duda afecta al LCP. En el caso de estos ejemplos, la solicitud de la imagen de LCP se retrasa significativamente en comparación con la experiencia equivalente renderizada por el servidor que no requiere que aparezca JavaScript.
Esto se desvía un poco del enfoque de este artículo, pero los efectos de renderizar el lenguaje de marcado en el cliente van mucho más allá de anular el escáner de precarga. Por un lado, introducir JavaScript para potenciar una experiencia que no lo requiere genera un tiempo de procesamiento innecesario que puede afectar la Interaction to Next Paint (INP). Es más probable que la renderización de cantidades extremadamente grandes de lenguaje de marcado en el cliente genere tareas largas en comparación con la misma cantidad de lenguaje de marcado que envía el servidor. El motivo de esto, además del procesamiento adicional que implica JavaScript, es que los navegadores transmiten el lenguaje de marcado desde el servidor y dividen la renderización de una manera que tiende a limitar las tareas largas. Por otro lado, el lenguaje de marcado renderizado por el cliente se controla como una sola tarea monolítica, lo que puede afectar el INP de una página.
La solución para esta situación depende de la respuesta a esta pregunta: ¿Hay algún motivo por el que el servidor no pueda proporcionar el lenguaje de marcado de tu página en lugar de renderizarlo en el cliente? Si la respuesta es "no", se debe considerar la renderización del servidor (SSR) o el lenguaje de marcado generado de forma estática siempre que sea posible, ya que ayudará al escáner de precarga a descubrir y recuperar de forma oportuna los recursos importantes con anticipación.
Si tu página necesita JavaScript para adjuntar funcionalidad a algunas partes del lenguaje de marcado de tu página, puedes hacerlo con SSR, ya sea con JavaScript básico o con hidratación para obtener lo mejor de ambos mundos.
Ayuda al escáner de precarga para que te ayude
El escáner de precarga es una optimización del navegador muy eficaz que ayuda a que las páginas se carguen más rápido durante el inicio. Si evitas los patrones que anulan su capacidad de descubrir recursos importantes con anticipación, no solo simplificas el desarrollo, sino que también creas mejores experiencias del usuario que generarán mejores resultados en muchas métricas, incluidas algunas métricas web esenciales.
En resumen, estos son los aspectos que debes tener en cuenta de esta publicación:
- El escáner de precarga del navegador es un analizador de HTML secundario que analiza antes que el principal si está bloqueado para descubrir de forma oportuna los recursos que puede recuperar antes.
- El escáner de precarga no puede descubrir los recursos que no están presentes en el lenguaje de marcado que proporciona el servidor en la solicitud de navegación inicial. Entre las formas en que se puede anular el escáner de precarga, se incluyen las siguientes:
- Inyectar recursos en el DOM con JavaScript, ya sean secuencias de comandos, imágenes, hojas de estilo o cualquier otro elemento que sería mejor que estuviera en la carga útil de lenguaje de marcado inicial del servidor
- Carga diferida de imágenes o iframes en la mitad superior de la página con una solución de JavaScript.
- Renderizar lenguaje de marcado en el cliente que puede contener referencias a recursos secundarios del documento con JavaScript
- El escáner de precarga solo analiza HTML. No examina el contenido de otros recursos, en particular CSS, que pueden incluir referencias a recursos importantes, incluidos los candidatos de LCP.
Si, por algún motivo, no puedes evitar un patrón que afecte negativamente la capacidad del escáner de precarga para acelerar el rendimiento de carga, considera la sugerencia de recursos rel=preload. Si usas rel=preload, realiza pruebas en las herramientas de laboratorio para asegurarte de que te brinde el efecto deseado. Por último, no precargues demasiados recursos, ya que, cuando priorizas todo, nada se prioriza.
Recursos
- Las "secuencias de comandos asíncronas" insertadas mediante secuencias de comandos se consideran perjudiciales
- Cómo el precargador del navegador hace que las páginas se carguen más rápido
- Carga previamente los recursos críticos para mejorar la velocidad de carga
- Establece conexiones de red con anticipación para mejorar la velocidad percibida de la página
- Optimiza el Largest Contentful Paint