Construyendo el DOM más rápido: análisis especulativo, asincrónico, diferido y precargado

11 abril, 2018 21:04 por

Esta es una traducción del artículo original publicado en el blog de Mozilla Hacks.

En 2017, la caja de herramientas para asegurar que tu página web cargue rápido incluye todo desde minificación y optimización de recursos hasta cacheo, uso de CDN, división de código y tree shaking (o eliminación de código muerto). Sin embargo, puedes obtener un gran aumento de rendimiento con solo unas pocas palabras clave y una estructuración cuidadosa de código, incluso si no aún no estás familiarizado con los conceptos mencionados anteriormente y no sabes cómo empezar.

El reciente estándar web <link rel="preload">, que te permite cargar recursos críticos de manera rápida, ya llegó a Firefox, lo que es una gran oportunidad para revisar algunos fundamentos y sumergirse en el desempeño asociado con analizar el DOM.

Comprender lo que pasa dentro de un navegador es la herramienta más poderosa para todos los desarrolladores web. Veremos cómo los navegadores interpretan tu código y cómo te ayudan a cargar las páginas más rápido con el análisis especulativo. Vamos a desglosar cómo funcionan defer y async, y cómo aprovechar la nueva palabra clave preload.

Bloques de construcción

HTML describe la estructura de una página web. Para darle sentido al HTML, los navegadores primero tienen que convertirlo en un formato que entiendan – el Modelo de Objeto del Documento, o DOM. El motor del navegador tiene una pieza especial de código llamada analizador que es usada para convertir datos de un formato a otro. Un analizador HTML convierte datos de HTML a DOM.

En HTML, la anidación define las relaciones padre-hijo entre las etiquetas. En el DOM, los objetos están enlazados en una estructura de datos de árbol capturando esas relaciones. Cada etiqueta HTML es representada por un nodo del árbol (un nodo DOM).

El navegador construye el DOM parte por parte. Tan pronto como lleguen los primeros trozos de código, éste empieza a analizar el HTML, agregando nodos a la estructura de árbol.

El DOM tiene dos roles: es la representación de objeto del documento HTML, y actúa como una interfaz conectando la página al mundo exterior, como JavaScript. Cuando llamas a document.getElementById(), el elemento que es retornado es un nodo DOM. Cada nodo DOM tiene muchas funciones que puedes usar para acceder a él y cambiarlo, y como consecuencia el usuario ve los cambios.

Los estilos CSS encontrados en una página web son mapeados dentro del CSSOM – el Modelo de Objeto de CSS. Es muy parecido al DOM, pero para el CSS en lugar del HTML. A diferencia del DOM, éste no puede ser construido incrementalmente. Ya que las reglas CSS pueden sobrescribirse unas con otras, el motor del navegador tiene que hacer cálculos complejos para determinar cómo aplicar el código CSS al DOM.

La historia de la etiqueta <script>

A medida que el navegador construye el DOM, si aparece una etiqueta <script>...</script> en el HTML, debe ejecutarla de inmediato. Si el script es externo, descarga el script primero.

En los viejos tiempos, para ejecutar un script, el análisis tenía que ser pausado. Se iniciaría de nuevo solo después que el motor de JavaScript ejecutara el código del script.

¿Por qué el análisis tiene que detenerse? Bueno, los scripts pueden cambiar tanto el HTML y su producto – el DOM. Los scripts pueden cambiar la estructura del DOM agregando nodos con document.createElement(). Para cambiar el HTML, los scripts pueden agregar contenido con la célebre función document.write(). Es célebre porque puede cambiar el HTML de formas que puede afectar aún más el análisis. Por ejemplo, la función puede insertar la apertura de una etiqueta de comentario que haga inválido todo el resto de HTML.

Los scripts también pueden consultar algo del DOM, y si esto pasa mientras el DOM todavía está siendo construido, podría retornar resultados inesperados.

documento.write() es una función heredada que puede romper tu página de formas inesperadas y no deberías usarla, a pesar que todavía los navegadores la soportan. Por estas razones, los navegadores han desarrollado técnicas sofisticadas para evitar los problemas de rendimiento causados por el bloqueo de scripts que veremos más adelante.

¿Qué hay del CSS?

JavaScript bloquea el análisis porque modifica el documento. El CSS no puede modificar el documento, entonces parece que no hay razón para bloquear el análisis. ¿Correcto?

Sin embargo, ¿qué pasa si un script pregunta por información del estilo que aún no ha sido cargada? El navegador no sabe qué es lo que el script quiere ejecutar – puede preguntar por cualquier cosa como el background-color del nodo DOM que depende de la hoja de estilo, o puede esperar acceder directamente al CSSOM.

Debido a esto, el CSS puede bloquear el análisis dependiendo del orden de las hojas de estilo externas y scripts en el documento. Si hay hojas de estilo externas localizadas antes de los scripts en el documento, la construcción de los objetos del DOM y del CSSOM pueden interferir una con la otra. Cuando el analizador obtiene una etiqueta de script, la construcción del DOM no puede continuar hasta que el JavaScript termine de ejecutarse, y el JavaScript no puede ser ejecutado hasta que el CSS sea descargado, analizado, y el CSSOM esté disponible.

Otra cosa a tener en mente es que incluso si el CSS no bloquea la construcción del DOM, éste bloquea el renderizado. El navegador no mostrará nada hasta que tenga tanto el DOM como el CSSOM. Esto es porque las páginas sin CSS a menudo no son usables. Si un navegador te mostrara una página desordenada sin CSS y luego, minutos más tarde, se convirtió en una página con estilos, el contenido cambiante y los cambios visuales repentinos generarían una experiencia de usuario turbulenta.

Esta mala experiencia de usuario tiene un nombre – Flash de Contenido Sin Estilo o FOUC (por sus siglas en inglés).

Para evitar estos problemas, debes intentar entregar el CSS tan pronto como sea posible. ¿Recuerdas la popular buena práctica de “estilos en la parte superior, scripts en la parte inferior? ¡Ahora sabes por qué estaba ahí!

De vuelta al futuro – análisis especulativo

Pausar el analizador cuando un script es encontrado, significa que cada script que se cargue retrazará el descubrimiento del resto de recursos que fueron en enlazados en el HTML.

Si tienes unas cuantos scripts e imágenes para cargar, por ejemplo –

<script src="slider.js"></script>
<script src="animate.js"></script>
<script src="cookie.js"></script>
<img src="slide1.png">
<img src="slide2.png">

– el proceso usado para esto era algo así:

Eso cambió al rededor de 2008 cuando IE introdujo algo llamado “lookahead downloader” (o descargador que mira hacia adelante). Esto es una manera de seguir descargando los archivos que eran necesarios mientras los scripts sincrónicos estaban siendo ejecutados. Firefox, Chrome y Safari lo siguieron pronto, y hoy muchos navegadores usan esta técnica bajo diferentes nombres. Chrome y Safari tiene “el scanner de precarga” y Firefox – el analizador especulativo.

La idea es: a pesar que no es seguro construir el DOM mientras se ejecuta un script, todavía se puede analizar el HTML para ver qué otros recursos necesitan ser descargados. Los archivos descubiertos son agregados a la lista y se empieza a descargarlos en segundo plano en conecciones paralelas. Al mismo tiempo que el script termina de ejecutarse, los archivos pueden haber sido descargados.

El gráfico de cascada para el ejemplo anterior ahora se ve como esto:

Las peticiones de descarga iniciadas de esta forma son llamadas “especulativas” ya que es posible que el script cambie la estructura del HTML (¿recuerdas document.write?), resultando en conjeturas desperdiciadas. Aunque esto es posible, no es común, y eso es por qué el análisis especulativo todavía da grandes mejoras en el redimiento.

Mientras otros navegadores solamente precargan los recursos enlazados de esta manera, en Firefox el analizador de HTML también ejecuta el algoritmo de construcción del árbol de DOM de manera especulativa. La ventaja es que cuando la especulación es exitosa, no hay necesidad de analizar el archivo de nuevo para componer el DOM. La desventaja es que hay más trabajo perdido si la especulación falla.

(Pre)cargando cosas

Esta manera de cargar los recursos ofrece un importante aumento del rendimiento, y tú no necesitas hacer algo especial para aprovecharlo. Sin embargo, como un desarrollador web, saber cómo funciona el analizador especulativo puede ayudarte a aprovecharlo al máximo.

El conjunto de cosas que pueden ser precargadas varía entre navegadores. La mayoría de navegadores precarga:

  • scripts
  • CSS externos
  • e imágenes de la etiqueta <img>

Firefox también precarga el atributo poster de los elementos de vídeo, mientras Chrome y Safari precargan las reglas @import de los estilos en línea.

Hay límites para la cantidad de archivos que un navegador puede descargar en paralelo. Los límites varían entre navegadores y dependen de muchos factores, como por ejemplo si estás descargando todos los archivos de uno o varios servidores y si estás usando el protocolo HTTP/1.1 o HTTP/2. Para renderizar la página tan rápido como sea posible, los navegadores optimizan las descargas asignando una prioridad para cada archivo. Para determinar esas prioridades, siguen complejos esquemas basados en el tipo de recurso, posición en la estructura, y el progreso de la página renderizando.

Mientras hace el análisis especulativo, el navegador no ejecuta los bloques de JavaScript en línea. Esto significa que no descubrirá nigún recurso de script insertado, y probablemente éstos sean los últimos en la cola de búsqueda.

var script = document.createElement('script');
script.src = "//somehost.com/widget.js";
document.getElementsByTagName('head')[0].appendChild(script);

Deberías hacer esto más fácil para los navegadores para acceder a los recursos importantes los más pronto posible. Puedes colocarlos en etiquetas HTML o incluir la carga del script en línea y al inicio del documento. Sin embargo, algunas veces quieres que algunos recursos se carguen después porque son menos importantes. En ese caso, puedes ocultarlos del análisis especulativo cargándolos con JavaScript después en el documento.

También puedes revisar esta guía en la MDN Web Docs sobre cómo optimizar tus páginas para el analizador especulativo.

defer y async

Aún, los scripts sincrónicos que bloquean al analizador representan un problema. Y no todos los scripts son igualmente importantes para la experiencia de usuario, como aquellos para seguimimento y análisis. ¿La solución? Hacer posible cargar estos scripts menos importantes, de manera asincrónica.

Los atributos defer y async fueron introducidos para darle a los desarrolladores una manera de decirle al navegador qué scripts manejar asincrónicamente.

Ambos atributos le dicen al navegador que puede continuar con el análisis del HTML mientras carga el script en “segundo plano”, y luego ejecutar el script después que cargue. De esta manera, la descarga del script no bloquea la construcción del DOM y el renderizado de la página. Resultado: el usuario puede ver la página antes que todos los scripts hayan finalizado su carga.

La diferencia entre defer y async es en qué momento se empezará a ejecutar los scripts.

defer fue introducido antes que async. Su ejecución empieza después que el análisis está completamente finalizado, pero antes que se dispare el evento DOMContentLoaded. Esto garantiza que los scripts serán ejecutados en el orden que aparezcan en el HTML y no bloquearán al analizador.

Los scripts con async se ejecutan en la primera oportunidad después que han finalizado la descarga y antes que el evento load de window. Esto significa que es posible (y probable) que los scripts async no sean ejecutados en el orden en que aparecen en el HTML. Esto también significa que pueden interrumpir la construcción DOM.

En donde sea que sean especificados, los scrips con async cargan con baja prioridad. Éstos a menudo cargan después que todos los scripts, sin bloquear la construcción de DOM. Sin embargo, si un script con async finaliza su descarga rápidamente, su ejecución puede bloquear la construcción del DOM y todos los scripts sincrónicos que terminan de descargase depués.

Nota: Los atributos async y defer funcionan solamente para scripts externos. Son ignorados si no hay src.

preload

async y defer son geniales si quieres posponer el manejo de algunos scripts, pero ¿qué pasa con las cosas en tu página web que son críticas para la experiencia del usuario? Los analizadores especulativos son útiles, pero precargan solamente algunos tipos de recursos y siguen su propia lógica. El objetivo general es obtener primero el CSS porque éste bloquea el renderizado. Los scripts sincrónicos siempre tendrán mayor prioridad ante los asincrónicos. Las imágenes visibles dentro de la ventana deberán ser descargadas antes que las que encuentran debajo. Y también hay fuentes, videos, imágenes SVG… En resumen, es complicado.

Como un autor, tú sabes qué recursos son más importantes para renderizar tu página. Algunos de éstos están incluso enterrados en CSS o scripts y eso puede hacer que al navegador le tome un momento antes que los descubra. Para aquellos recursos importantes ahora puedes usar  <link rel="preload"> para comunicar al navegador que quieres cargarlos tan pronto como sea posible.

Todo lo que necesitas es escribir:

<link rel="preload" href="very_important.js" as="script">

Puedes enlazar casi cualquier cosa y el atributo as le dice al navegador que será descargado. Algunos de los posibles valores son:

  • script
  • style
  • image
  • font
  • audio
  • video

Puedes revisar el resto de tipos de contenido en MDN.

Las fuentes son probablemente la cosa más importante que está oculta en CSS. Éstas son críticas para renderizar el texto en la página, pero no pueden ser descargadas hasta que el navegador esté seguro que van a ser usadas. Esta verificación sucede solamente después que el CSS ha sido analizado, aplicado, y que el navegador haya relacionado las reglas CSS con los nodos DOM. Esto pasa bastante tarde en el proceso de carga de la página y resulta en una demora innecesaria en el renderizado del texto. Puedes evitarlo usando el atributo preload cuando enlazas las fuentes.

Una cosa a la que hay que prestar atención cuando precargas fuentes es que también tienes que establecer el atributo crossorigin incluso si las fuentes vienen desde el mismo dominio:

<link rel="preload" href="font.woff" as="font" crossorigin>

La funcionalidad de precarga tiene soporte limitado en los navegadores actualmente, pero puedes revisar el progreso aquí.

Conclusión

Los navegadores son bestias complejas que han estado evolucionando desde los años 90. Hemos cubierto algunas de las peculiaridades desde el legado y de los nuevos estándares en el desarrollo web. Escribiendo tu código con estas guías te ayudará para seleccionar las mejores estrategias para entregar una experiencia de navegación fluida.

Si estás emocionado por aprender más sobre cómo los navegadores funcionan aquí hay algunos otros artículos que debes revisar:

Quantum de cerca: ¿qué es el motor de un navegador web?

Dentro de un super rápido motor CSS: Quantum CSS (alias Stylo)

The following two tabs change content below.

AngelFQC

Web Developer at BeezNest Latino
Ingeniero de Sistemas y Computación. Desarrollador PHP. Mozilla Peru. Chamilo LMS Developer.

Compartir artículo:

Join the discussion at foro.mozilla-hispano.org

  • ¡Participa!

    Firefox Friends »
    Agrega botones de Firefox en tu sitio web y comparte tu amor por Mozilla Firefox.
    Ayuda a otros usuarios en Twitter.
    Colabora con la comunidad »
    En Mozilla lo importante son las personas. Descubre cómo puedes colaborar.

    Boletín Firefox

    Suscríbete al boletín de novedades de Firefox.

  • Descargas

    Descarga los programas de Mozilla.

    Lo más visto

    cc-by-sa