Fecha de publicación: 31 de diciembre de 2013
JavaScript nos permite modificar prácticamente 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 presentación de la página. Para obtener un rendimiento óptimo, haz que tu JavaScript sea asíncrono y elimina todo JavaScript innecesario de la ruta de acceso de representació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íncrono.
JavaScript es un lenguaje dinámico que se ejecuta en un navegador y nos permite modificar prácticamente todos los aspectos del comportamiento de la página: podemos modificar contenido de la página agregando o eliminando elementos del árbol del DOM. También podemos modificar las propiedades del CSSOM de cada elemento y controlar las entradas del usuario, entre otras muchas opciones más. Para ilustrar esto, observa lo que sucede cuando se cambia el ejemplo anterior de "Hello World" para agregar una secuencia de comandos integrada breve:
<!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 explorar el DOM y obtener la referencia al nodo de intervalo oculto. Es posible que el nodo no esté visible en el árbol de renderización, pero aún se encuentra en el DOM. Luego, cuando tengamos la referencia, podremos cambiar su texto (mediante .textContent) e incluso anular su propiedad de estilo de visualización calculada pasando de “none” a “inline”. Ahora nuestra página muestra “Hello interactive students!”.
JavaScript también nos permite crear nuevos elementos, aplicarles ajustes de estilo, también agregarlos al DOM y eliminarlos de él. Técnicamente, toda nuestra página podría ser un solo archivo JavaScript grande que nos permita crear los elementos y darles estilo uno por uno. Si bien esto funciona, en la práctica es mucho más fácil usar HTML y CSS. 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 de CSS de un nodo del DOM existente y agregamos un nodo completamente nuevo al documento. Nuestra página no ganará ningún premio de diseño, pero en ella se ilustrarán la capacidad y flexibilidad que JavaScript nos ofrece.
Sin embargo, si bien JavaScript nos brinda mucha capacidad, genera muchas limitaciones adicionales respecto de cómo y cuándo se representa la página.
Primero, ten en cuenta que en el ejemplo anterior nuestra secuencia de comandos integrada 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>
, verás que se producirá un error en esta y se te advertirá de que no puedes 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 donde 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 delega el control al motor JavaScript. Una vez que este último termina de ejecutarse, el navegador reanuda el proceso y reanuda la construcción del DOM.
En otras palabras, nuestro bloque de secuencia de comandos no podrá encontrar elementos más adelante en la página, ya que aún no estarán procesados. 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 la introducción de secuencias de comandos en nuestra página es que pueden leer y modificar no solo el DOM, sino también las propiedades de 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é ocurriría si el navegador no ha terminado de descargar y compilar el CSSOM cuando queramos ejecutar nuestra secuencia de comandos? La respuesta no es muy buena para el rendimiento: el navegador demora la ejecución de la secuencia de comandos y la construcción del DOM hasta haber terminado de descargar y construir el CSSOM.
En resumen, JavaScript introduce muchas dependencias nuevas entre la ejecución del DOM, el CSSOM y JavaScript. Esto puede tener efectos adversos importantes en la velocidad con que el navegador procese y represente la página en la pantalla:
- La ubicación de la secuencia de comandos en el documento es importante.
- Cuando el navegador encuentra una etiqueta de secuencia de comandos, se pausa 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 pausa hasta que el CSSOM esté listo.
Cuando hablamos de "optimizar la ruta de acceso de representación crítica", en gran medida nos referimos a la comprensión y optimización del gráfico de dependencias entre HTML, CSS y JavaScript.
Bloquear el 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. Vimos esto en acción con una secuencia de comandos integrada 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é ocurre con las secuencias de comandos que se incluyen mediante una etiqueta script? Toma el ejemplo anterior y extrae el código en un archivo independiente:
<!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, deberías esperar que ambos se comporten 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 que se recupere la secuencia de comandos del disco, el caché o un servidor remoto, lo que puede sumar cientos de milisegundos de demora a la ruta de renderización crítica.
De forma predeterminada, JavaScript bloquea a los analizadores. Dado que el navegador no sabe qué es 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 que indica que no es necesario ejecutar la secuencia de comandos en el punto exacto en el que 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 recuperar el archivo de la caché o de un servidor remoto.
Para lograrlo, se agrega el atributo async
al elemento <script>
:
<!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 secuencia de comandos indica al navegador que no bloquee la construcción del DOM mientras espera que la secuencia de comandos esté disponible, lo que puede mejorar el rendimiento de manera significativa.