Descubre qué es el verificador de precarga del navegador, cómo ayuda al rendimiento y cómo puedes evitarlo.
Un aspecto que se suele pasar por alto en la optimización de la velocidad de la página es conocer un poco sobre el funcionamiento interno del navegador. Los navegadores realizan ciertas optimizaciones para mejorar el rendimiento de formas que nosotros, como desarrolladores, no podemos, pero solo mientras esas optimizaciones no se vean frustradas de forma involuntaria.
Una optimización del navegador interno que debes comprender es el analizador previo a la carga del navegador. En esta publicación, se explicará cómo funciona el verificador de carga previa y, lo que es más importante, cómo puedes evitar que te obstaculice.
¿Qué es un escáner de carga previa?
Todos los navegadores tienen un analizador de HTML principal que tokeniza el lenguaje de marcado sin procesar y lo convierte en un modelo de objeto. 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 algo de él) hasta que se descargue y analice el CSS.
En el caso de los archivos CSS, el procesamiento se bloquea para evitar un destello de contenido sin diseño (FOUC), que se produce 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.
El motivo es que el navegador no puede saber con certeza si un script determinado modificará el DOM mientras el analizador de HTML principal sigue funcionando. Por este motivo, se suele cargar el código JavaScript al final del documento para que los efectos del bloqueo del análisis y el procesamiento sean marginales.
Estos son buenos motivos por los que el navegador debe bloquear el análisis y la renderización. Sin embargo, bloquear cualquiera de estos pasos importantes no es deseable, ya que pueden retrasar el programa 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 carga previa puede anticiparse 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.
El rol de un escáner de carga previa es especulativo, lo que significa que examina el lenguaje de marcado sin procesar para encontrar recursos que se puedan recuperar de forma oportunista antes de que el analizador de HTML principal los descubra.
Cómo saber cuándo funciona el escáner de carga previa
El analizador de precarga existe porque se bloquea la renderización y el análisis. Si estos dos problemas de rendimiento nunca hubieran existido, el verificador de carga previa 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 una demora artificial en las solicitudes para saber dónde funciona el escáner de carga previa.
Tomemos como ejemplo esta página de texto e imágenes básicos con una hoja de estilo. Dado que los archivos CSS bloquean la renderización y el análisis, se introduce una demora artificial de dos segundos para la hoja de estilo a través de un servicio de proxy. Este retraso permite ver con mayor facilidad en la cascada de red dónde está funcionando el verificador de carga previa.
Como puedes ver en el diagrama de cascada, el escáner de precarga descubre el elemento <img> incluso mientras se bloquea 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.
Ahora que ya vimos este ejemplo sencillo, analicemos algunos patrones del mundo real en los que se puede burlar el escáner de carga previa y qué se puede hacer para corregirlos.
Secuencias de comandos async insertadas
Supongamos que tienes código HTML en tu <head> que incluye 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 inserta 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 detectar la secuencia de comandos durante la fase de bloqueo de renderización, ya que se inserta en el cliente.
Desglosemos lo que sucedió aquí:
- En el segundo 0, se solicita el documento principal.
- A los 1.4 s, llega el primer byte de la solicitud de navegación.
- A los 2.0 segundos, se solicitan el CSS y la imagen.
- Dado que el analizador está bloqueado mientras carga la hoja de estilo y el código JavaScript intercalado que inyecta la secuencia de comandos
asyncaparece 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 del script se produce solo después de que finaliza la descarga de la hoja de estilo. Esto retrasa la ejecución de la secuencia de comandos lo antes posible. En cambio, como el elemento <img> se puede detectar en el lenguaje de marcado proporcionado por el servidor, el escáner de carga previa lo detecta.
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.
Es posible que sientas la tentación de sugerir que estos problemas se podrían solucionar con rel=preload. Esto sin duda 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 inyectando un elemento <script> en el DOM?
async insertada, pero la secuencia de comandos async se precarga para garantizar que se descubra antes.
La precarga "soluciona" el problema aquí, pero introduce uno nuevo: la secuencia de comandos async en las dos primeras demostraciones, a pesar de cargarse en <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 precarga la secuencia de comandos async, la hoja de estilo sigue cargándose con la prioridad "Más alta", pero la prioridad de la secuencia de comandos se promovió 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 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 carga previa insertándola en el DOM. Experimenta según sea necesario con la ubicació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 a menudo se aplica a las imágenes. Sin embargo, a veces, la carga diferida se aplica de forma incorrecta a las imágenes que se encuentran en la mitad superior de la página, por así decirlo.
Esto genera posibles problemas con la detección de recursos en lo que respecta al analizador previo a la carga y puede retrasar innecesariamente el tiempo que se tarda en detectar una referencia a una imagen, descargarla, decodificarla y presentarla. Tomemos este ejemplo 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 potenciados por JavaScript. Cuando la imagen se desplaza al 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 se encuentran en el viewport durante el inicio. Dado que el escáner de carga previa 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 carga de la imagen se retrasa hasta después de que se descargue, compile y ejecute el JavaScript del cargador diferido.
Según el tamaño de la imagen, que puede depender del tamaño del viewport, es posible que sea un elemento candidato para el Largest Contentful Paint (LCP). Cuando el escáner de carga previa 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 se encuentran en el viewport durante el inicio, ya que el analizador de precarga descubrirá y recuperará el recurso de imagen más rápido.
En este ejemplo simplificado, el resultado es una mejora de 100 milisegundos en el LCP en una conexión lenta. Esto puede no parecer una gran mejora, pero lo es si se tiene en cuenta 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 a LCP pueden tener que competir por el ancho de banda con muchos otros recursos, por lo que las optimizaciones como esta son 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 se hace referencia en la propiedad background-image.
Al igual que el HTML, los navegadores procesan el 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 analizador previo a la carga.
Supongamos que el candidato a LCP de tu página es un elemento con una propiedad background-image de CSS. A continuación, se describe lo que sucede a medida que 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 carga previa no se ve afectado, sino que no participa. Aun así, si un candidato a LCP en la página proviene de una propiedad CSS background-image, querrás 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 rel=preload es pequeña, pero ayuda al navegador a descubrir la imagen antes de lo que lo haría de otro modo:
background-image de CSS (fila 3). La sugerencia rel=preload ayuda al navegador a descubrir la imagen alrededor de 250 milisegundos antes que sin la sugerencia.
Con la sugerencia rel=preload, el candidato a 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 al LCP debe cargarse desde CSS. Con una etiqueta <img>, tendrás más control sobre la carga de una imagen adecuada para la ventana gráfica y permitirás que el escáner de carga previa la descubra.
Se insertaron demasiados recursos
La inserción es una práctica que coloca un recurso dentro del HTML. Puedes insertar hojas de estilo en elementos <style>, secuencias de comandos en elementos <script> y prácticamente cualquier otro recurso con codificación en Base64.
Insertar recursos puede ser más rápido que descargarlos, ya que no se emite una solicitud separada para el recurso. Está en el documento y se carga al instante. 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 son reutilizables.
- Incluso si puedes almacenar en caché el código 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 insertas demasiado contenido intercalado, retrasas el descubrimiento de recursos por parte del analizador de precarga más adelante en el documento, ya que la descarga de ese contenido intercalado adicional lleva más tiempo.
Tomemos esta página como ejemplo. En ciertas condiciones, el candidato a LCP es la imagen que se encuentra en la parte superior de la página, y el CSS está en un archivo independiente que carga un elemento <link>. La página también usa cuatro fuentes web que se solicitan como archivos separados del recurso CSS.
<img>, pero el escáner de precarga la descubre porque el CSS y las fuentes necesarios para la carga de la página se encuentran en recursos separados, 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 el elemento `` retrasa el descubrimiento de la imagen por parte del analizador de precarga hasta que esos recursos se descarguen por completo.
El impacto de la inserción directa genera consecuencias negativas para el LCP en este ejemplo y para el rendimiento en general. La versión de la página que no inserta nada de forma intercalada renderiza la imagen del LCP en aproximadamente 3.5 segundos. La página que inserta todo no renderiza la imagen del LCP hasta poco más de 7 segundos.
Aquí hay más en juego que solo el escáner de carga previa. La inserción de fuentes no es una buena estrategia porque Base64 es un formato ineficiente para los recursos binarios. Otro factor en juego es que los recursos de fuentes externas no se descargan a menos que el CSSOM los considere necesarios. Cuando esas fuentes se insertan como Base64, se descargan independientemente de si se necesitan para la página actual.
¿Podría mejorar la situación una precarga? Claro. Podrías precargar la imagen del LCP y reducir el tiempo del LCP, pero inflar tu HTML potencialmente no almacenable en caché con recursos intercalados tiene otras consecuencias negativas en el rendimiento. El First Contentful Paint (FCP) también se ve afectado por este patrón. En la versión de la página en la que no se inserta nada, 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 con la inserción de elementos en el código HTML, en especial los recursos codificados en Base64. En general, no se recomienda, excepto para recursos muy pequeños. Incluye la menor cantidad posible de código intercalado, ya que incluir demasiado es peligroso.
Cómo renderizar el lenguaje de marcado con JavaScript del cliente
No hay dudas: JavaScript afecta la velocidad de la página. Los desarrolladores no solo dependen de él para proporcionar interactividad, sino que también se ha tendido a confiar en él para entregar el contenido en sí. Esto mejora la experiencia del desarrollador de alguna manera, pero los beneficios para los desarrolladores no siempre se traducen en beneficios para los usuarios.
Un patrón que puede derrotar al escáner de carga previa es renderizar el lenguaje de marcado con JavaScript del cliente:
Cuando las cargas útiles de marcado se incluyen y renderizan por completo con JavaScript en el navegador, todos los recursos de ese marcado son efectivamente invisibles para el escáner de precarga. Esto retrasa el descubrimiento de recursos importantes, lo que sin duda afecta el LCP. En el caso de estos ejemplos, la solicitud de la imagen del 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 del lenguaje de marcado de renderización en el cliente van mucho más allá de derrotar al escáner de carga previa. Por un lado, introducir JavaScript para potenciar una experiencia que no lo requiere introduce 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 tal manera que tienden 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 puede 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 esto ayudará al analizador de carga previa a descubrir y recuperar de forma oportunista recursos importantes con anticipación.
Si tu página necesita JavaScript para adjuntar funcionalidad a algunas partes del lenguaje de marcado de la página, puedes hacerlo con SSR, ya sea con JavaScript simple o con hidratación para obtener lo mejor de ambos mundos.
Ayuda al verificador de precarga
El verificador de carga previa 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 patrones que impidan que descubra recursos importantes con anticipación, no solo simplificarás el desarrollo, sino que también crearás mejores experiencias del usuario que generarán mejores resultados en muchas métricas, incluidas algunas métricas web esenciales.
En resumen, estos son los puntos clave que debes recordar de esta publicación:
- El escáner de precarga del navegador es un analizador de HTML secundario que analiza antes del principal si está bloqueado para descubrir de forma oportunista los recursos que puede recuperar antes.
- El scanner de carga previa 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 eludir el análisis previo a la carga, se incluyen las siguientes (sin limitaciones):
- Insertar recursos en el DOM con JavaScript, ya sean secuencias de comandos, imágenes, hojas de estilo o cualquier otro elemento que sería mejor incluir en la carga útil de marcado inicial del servidor
- Carga diferida de imágenes o iframes visibles sin necesidad de desplazarse con una solución de JavaScript
- Renderizar el lenguaje de marcado en el cliente, que puede contener referencias a subrecursos del documento con JavaScript
- El escáner de precarga solo analiza el código HTML. No examina el contenido de otros recursos, en particular, CSS, que pueden incluir referencias a recursos importantes, incluidos los candidatos a LCP.
Si, por algún motivo, no puedes evitar un patrón que afecte negativamente la capacidad del verificador de carga previa para acelerar el rendimiento de la carga, considera la sugerencia de recurso 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 cargues previamente demasiados recursos, ya que, si priorizas todo, no priorizarás nada.
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
- Precarga 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
- Cómo optimizar el Largest Contentful Paint
Imagen hero de Unsplash, de Mohammad Rahmani .