JavaScript nos permite modificar casi todos los aspectos de la página: el contenido, el estilo y su respuesta ante la interacción del usuario. Sin embargo, JavaScript también puede bloquear la construcción del DOM y demorar la renderización de la página. Para obtener un rendimiento óptimo, haz que tu JavaScript sea asíncrono y quita cualquier JavaScript innecesario de la ruta de acceso de renderización crítica.
Resumen
- JavaScript puede consultar y modificar el DOM y el CSSOM.
- La ejecución de JavaScript bloquea el CSSOM.
- JavaScript bloquea la construcción del DOM a menos que se declare explícitamente como asíncrona.
JavaScript es un lenguaje dinámico que se ejecuta en un navegador y nos permite modificar casi todos los aspectos del comportamiento de la página: podemos modificar el contenido agregando y quitando elementos del árbol del DOM; podemos modificar las propiedades del CSSOM de cada elemento; controlar las entradas del usuario, y mucho más. Para ilustrar esto, ampliemos nuestro ejemplo anterior "Hello World" con una sencilla secuencia de comandos integrada:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
JavaScript nos permite acceder al DOM y obtener la referencia al nodo de intervalo oculto. Es posible que el nodo no se pueda ver en el árbol de representación, pero aún está en el DOM. Luego, cuando tengamos la referencia, podemos cambiar su texto (mediante .textContent) e incluso anular su propiedad de estilo de visualización calculada de "none" a "inline". Ahora nuestra página muestra "Hello Interactive students!".
JavaScript también nos permite crear nuevos elementos, aplicarles ajustes de estilo, quitarlos y agregarles estilo al DOM. Técnicamente, toda nuestra página podría ser solo un gran archivo JavaScript que cree los elementos y les aplique estilo uno por uno. Si bien funcionaría, en la práctica, usar HTML y CSS es mucho más fácil. En la segunda parte de nuestra función de JavaScript, creamos un nuevo elemento div, configuramos su contenido de texto, le aplicamos un estilo y lo anexamos al cuerpo.
Con esto, modificamos el contenido y el estilo CSS de un nodo del DOM existente, y agregamos un nodo totalmente nuevo al documento. Nuestra página no ganará ningún premio de diseño, pero en ella se ilustra la potencia y flexibilidad que nos ofrece JavaScript.
No obstante, aunque JavaScript nos brinda mucha potencia, genera muchas limitaciones adicionales respecto de cómo y cuándo se representa la página.
Primero, observa que en el ejemplo anterior nuestra secuencia de comandos intercalada se encuentra cerca de la parte inferior de la página. ¿Por qué? Deberías probarlo tú mismo, pero si movemos la secuencia de comandos por encima del elemento span, notarás que la secuencia de comandos falla y indica que no puede encontrar una referencia a ningún elemento span en el documento. Es decir, getElementsByTagName(‘span') muestra null. Esto demuestra una propiedad importante: nuestra secuencia de comandos se ejecuta en el punto exacto en que se inserta en el documento. Cuando el analizador de HTML encuentra una etiqueta de secuencia de comandos, pausa su proceso de construcción del DOM y le proporciona el control al motor de JavaScript. Una vez que el motor de JavaScript termina de ejecutarse, el navegador retoma el proceso desde donde lo dejó y reanuda la construcción del DOM.
En otras palabras, nuestro bloque de secuencia de comandos no podrá encontrar ningún elemento más adelante en la página porque todavía no se procesó. Dicho de otro modo: la ejecución de nuestra secuencia de comandos intercalada bloquea la construcción del DOM, lo que también retrasa la renderización inicial.
Otra propiedad sutil de introducir secuencias de comandos en nuestra página es que pueden leer y modificar no solo el DOM, sino también las propiedades del CSSOM. De hecho, eso es exactamente lo que estamos haciendo en nuestro ejemplo cuando cambiamos la propiedad de visualización del elemento span de none a inline. ¿El resultado final? Ahora tenemos una condición de carrera.
¿Qué sucede si el navegador no ha terminado de descargar y compilar el CSSOM cuando queramos ejecutar nuestra secuencia de comandos? La respuesta es simple y no muy buena para el rendimiento: el navegador retrasa la ejecución de la secuencia de comandos y la construcción del DOM hasta que termina de descargar y construir el CSSOM.
En resumen, JavaScript presenta muchas dependencias nuevas entre la ejecución del DOM, el CSSOM y JavaScript. Esto puede provocar que el navegador tarde de forma significativa en el procesamiento y la renderización de la página en la pantalla:
- La ubicación de la secuencia de comandos en el documento es significativa.
- Cuando el navegador encuentra una etiqueta de secuencia de comandos, se detiene la construcción del DOM hasta que la secuencia de comandos termina de ejecutarse.
- JavaScript puede consultar y modificar el DOM y el CSSOM.
- La ejecución de JavaScript se detiene hasta que el CSSOM esté listo.
En gran medida, “optimizar la ruta de representación crítica” hace referencia a la comprensión y optimización del gráfico de dependencias entre HTML, CSS y JavaScript.
Bloqueo del analizador en comparación con JavaScript asíncrono
De forma predeterminada, la ejecución de JavaScript "bloquea el analizador": cuando el navegador encuentra una secuencia de comandos en el documento, debe pausar la construcción del DOM, delegar el control al tiempo de ejecución de JavaScript y permitir que la secuencia de comandos se ejecute antes de continuar con la construcción del DOM. Ya vimos esto en acción con una secuencia de comandos intercalada en nuestro ejemplo anterior. De hecho, las secuencias de comandos intercaladas siempre bloquean el analizador, a menos que escribas código adicional para diferir su ejecución.
¿Qué sucede con las secuencias de comandos que se incluyen mediante una etiqueta de secuencia de comandos? Volvamos a nuestro ejemplo anterior y extraigamos el código en un archivo separado:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script External</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
app.js
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
Ya sea que usemos una etiqueta <script> o un fragmento de JavaScript intercalado, ambos se comportarían de la misma manera. En ambos casos, el navegador detiene y ejecuta la secuencia de comandos antes de poder procesar el resto del documento. Sin embargo, en el caso de un archivo JavaScript externo, el navegador debe pausarse para esperar a que se recupere la secuencia de comandos del disco, la caché o un servidor remoto, lo que puede agregar decenas a miles de milisegundos de demora a la ruta de renderización crítica.
De forma predeterminada, todo JavaScript bloquea el analizador. Como el navegador no sabe lo que planea hacer la secuencia de comandos en la página, supone el peor de los casos y bloquea el analizador. Una señal al navegador de que no es necesario ejecutar la secuencia de comandos en el punto exacto donde se hace referencia permite que el navegador continúe construyendo el DOM y deja que la secuencia de comandos se ejecute cuando esté lista; por ejemplo, después de que se recupera el archivo de la caché o un servidor remoto.
Para lograrlo, marcamos nuestra secuencia de comandos como async:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script Async</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Agregar la palabra clave "async" a la etiqueta de la secuencia de comandos le indica al navegador que no bloquee la construcción del DOM mientras espera que la secuencia de comandos esté disponible, lo cual puede mejorar el rendimiento de manera significativa.