Sumérgete en las oscuras aguas de la carga de secuencias de comandos

Introducción

En este artículo, te enseñaré a cargar JavaScript en el navegador y ejecutarlo.

No, espera, regresa. Sé que suena mundano y simple, pero recuerda que esto ocurre en el navegador, en el que lo teórico simple se convierte en una novedad impulsada por el legado. Conocer estas peculiaridades te permite elegir la forma más rápida y menos molesta de cargar secuencias de comandos. Si tienes poco tiempo, ve a la referencia rápida.

Para empezar, la especificación define las distintas formas en que una secuencia de comandos puede descargarse y ejecutarse:

El mensaje WHWG cuando se carga la secuencia de comandos
WhatWG sobre la carga de secuencias de comandos

Al igual que todas las especificaciones de WHG, al principio parece el resultado de una bomba de cúmulo en una fábrica de scrabble, pero, una vez que lo lees por quinta vez y te limpiaste la sangre de los ojos, la verdad es que resulta muy interesante:

Mi primer guion incluye

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Ah, la maravillosa simplicidad. Aquí, el navegador descargará ambas secuencias de comandos en paralelo y las ejecutará lo antes posible, manteniendo su orden. “2.js” no se ejecutará hasta que se ejecute “1.js” (o no lo haga), “1.js” no se ejecutará hasta que se haya ejecutado la secuencia de comandos o la hoja de estilo anterior, etc.

Lamentablemente, mientras tanto, el navegador bloquea la renderización adicional de la página. Esto se debe a las APIs de DOM de "la primera era de la Web" que permiten agregar cadenas al contenido que el analizador está masticando, como document.write. Los navegadores más nuevos seguirán analizando o analizando el documento en segundo plano y activarán la descarga de contenido externo que puedan necesitar (js, imágenes, CSS, etc.), pero la renderización sigue bloqueada.

Es por eso que lo bueno y lo bueno del mundo del rendimiento recomienda colocar elementos de secuencias de comandos al final de tu documento, ya que bloquea la menor cantidad de contenido posible. Desafortunadamente, esto significa que el navegador no verá tu secuencia de comandos hasta que descargue todo el código HTML. A partir de ese momento, habrá comenzado a descargar otro contenido, como iframes, imágenes y CSS. Los navegadores modernos son suficientemente inteligentes para dar prioridad a JavaScript sobre las imágenes, pero podemos hacerlo mejor.

Gracias, Irlanda. (no, no estoy siendo sarcástica)

<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>

Microsoft reconoció estos problemas de rendimiento e introdujo la opción "diferir" en Internet Explorer 4. En pocas palabras, dice: "Prometo no inyectar elementos en el analizador con elementos como document.write. Si no cumplo esa promesa, puedes castigarme de la forma que creas conveniente". Este atributo se convirtió en HTML4 y apareció en otros navegadores.

En el ejemplo anterior, el navegador descargará ambas secuencias de comandos en paralelo y las ejecutará justo antes de que se active DOMContentLoaded, manteniendo el orden.

Como una bomba de cúmulo en una fábrica de ovejas, "difer" se convirtió en un desastre. Entre los atributos “src” y “defer”, las etiquetas de secuencias de comandos y las que se agregaron de forma dinámica, tenemos 6 patrones para agregar una secuencia de comandos. Por supuesto, los navegadores no estuvieron de acuerdo con el orden en el que debían ejecutarse. Mozilla escribió un excelente artículo sobre el problema para detenerlo en 2009.

El WHG hizo que el comportamiento fuera explícito y declaraba que “diferir” no tenía ningún efecto en las secuencias de comandos que se agregaban de forma dinámica o que carecían de “src”. De lo contrario, las secuencias de comandos aplazadas deberían ejecutarse después de analizar el documento, en el orden en el que se agregaron.

Gracias, Irlanda. (ahora estoy siendo sarcástica)

Da y elimina. Lamentablemente, hay un error molesto en IE4-9 que puede hacer que las secuencias de comandos se ejecuten en un orden inesperado. Esto es lo que ocurre:

1.js

console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');

2.js

console.log('3');

Si suponemos que hay un párrafo en la página, el orden esperado de los registros es [1, 2, 3], aunque en IE9 y en versiones anteriores verás [1, 3, 2]. Algunas operaciones del DOM específicas provocan que IE pause la ejecución actual de la secuencia de comandos y ejecute otras secuencias de comandos pendientes antes de continuar.

Sin embargo, incluso en implementaciones que no presentan errores, como IE10 y otros navegadores, la ejecución de secuencias de comandos se retrasa hasta que se descarga y analiza todo el documento. Esto puede ser conveniente si vas a esperar a DOMContentLoaded de todos modos, pero si quieres ser realmente agresivo con el rendimiento, puedes comenzar a agregar objetos de escucha y realizar un arranque más rápido...

HTML5 al rescate

<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>

HTML5 nos proporcionó un nuevo atributo, “async”, que supone que no usarás document.write, pero no espera hasta que el documento se haya analizado para ejecutarse. El navegador descargará ambas secuencias de comandos en paralelo y las ejecutará lo antes posible.

Lamentablemente, dado que se ejecutarán lo antes posible, “2.js” se puede ejecutar antes que “1.js”. Esto está bien si son independientes, quizás “1.js” es una secuencia de comandos de seguimiento que no tiene nada que ver con “2.js”. Pero si tu “1.js” es una copia CDN de jQuery de la que depende “2.js”, tu página no tendrá errores en esta página...

Sé lo que necesitamos: una biblioteca de JavaScript.

Lo ideal es que un conjunto de secuencias de comandos se descarguen de inmediato sin bloquear la renderización y se ejecuten lo antes posible en el orden en que se agregaron. Lamentablemente, HTML te odia y no te permite hacer eso.

JavaScript abordado el problema de algunas maneras. Algunos requerían que realizaras cambios en tu JavaScript, uniéndolo a una devolución de llamada que la biblioteca llama en el orden correcto (p. ej., RequireJS). Otros usaban XHR para descargar en paralelo y, luego, eval() en el orden correcto, lo que no funcionaba para las secuencias de comandos en otro dominio, a menos que tuvieran un encabezado CORS y lo admitira el navegador. Algunos incluso usaron trucos supermágicos, como LabJS.

Los hackeos implicaron engañar al navegador para que descargue el recurso de manera que activara un evento cuando se complete, pero evitara ejecutarlo. En LabJS, la secuencia de comandos se agregaría con un tipo de MIME incorrecto, p. ej., <script type="script/cache" src="...">. Una vez descargadas todas las secuencias de comandos, se volvían a agregar con el tipo correcto, con la esperanza de que el navegador las obtuviera directamente de la caché y las ejecutara inmediatamente, en orden. Esto dependía de un comportamiento conveniente pero no especificado, y se rompía cuando los navegadores que declaraban HTML5 no debían descargar secuencias de comandos con un tipo no reconocido. Es importante destacar que LabJS se adaptó a estos cambios y ahora utiliza una combinación de los métodos que se mencionan en este artículo.

Sin embargo, los cargadores de secuencias de comandos tienen un problema de rendimiento propio, por lo que debes esperar a que se descargue y analice el código JavaScript de la biblioteca para poder comenzar a descargar las secuencias de comandos que administra. Además, ¿cómo cargaremos el cargador de secuencias de comandos? ¿Cómo cargaremos la secuencia de comandos que le indica al cargador de secuencias de comandos qué cargar? ¿Quiénes miran a los Watchmen? ¿Por qué estoy desnuda? Todas estas son preguntas difíciles.

Básicamente, si tiene que descargar un archivo de secuencia de comandos adicional antes de pensar en descargar otras secuencias de comandos, ha perdido la batalla de rendimiento.

El DOM al rescate

La respuesta está en la especificación HTML5, aunque está oculta en la parte inferior de la sección de carga de secuencias de comandos.

Tradujémoslo a "Earthling":

[
  '//other-domain.com/1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  document.head.appendChild(script);
});

Las secuencias de comandos que se crean y agregan al documento de forma dinámica son asíncronas de forma predeterminada. No bloquean la renderización y se ejecutan tan pronto como se descargan, lo que significa que podrían salir en el orden incorrecto. Sin embargo, podemos marcarlos de forma explícita como que no son asíncronas:

[
  '//other-domain.com/1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

Esto proporciona a nuestras secuencias de comandos una combinación de comportamiento que no se puede lograr con HTML simple. Al ser explícitamente no asíncronas, las secuencias de comandos se agregan a una cola de ejecución, la misma a la que se agregan en nuestro primer ejemplo de HTML sin formato. Sin embargo, al crearse de forma dinámica, se ejecutan fuera del análisis del documento, por lo que la renderización no se bloquea mientras se descargan (no confundas la carga de secuencias de comandos no asíncrona con la sincronización XHR, lo cual nunca es bueno).

La secuencia de comandos anterior se debe incluir intercalada en el encabezado de las páginas, la secuencia de comandos se pone en cola lo antes posible sin interrumpir la renderización progresiva y se ejecuta lo antes posible en el orden que especificaste. El archivo “2.js” se puede descargar de forma gratuita antes que “1.js”, pero no se ejecutará hasta que “1.js” se haya descargado y ejecutado correctamente, o hasta que no pueda realizar ninguna de las acciones anteriores. ¡Hurra! async-download pero orden-execution.

La carga de secuencias de comandos de esta manera es compatible con todo lo que admita el atributo asíncrono, excepto Safari 5.0 (5.1 está bien). Además, todas las versiones de Firefox y Opera son compatibles como versiones que no admiten el atributo asíncrono, de manera conveniente, ejecutan las secuencias de comandos agregadas dinámicamente en el orden en que se agregan al documento.

Esa es la forma más rápida de cargar secuencias de comandos, ¿verdad? ¿Cierto?

Bien, si decide de forma dinámica qué secuencias de comandos cargar, sí, de lo contrario, tal vez no. Con el ejemplo anterior, el navegador tiene que analizar y ejecutar la secuencia de comandos para descubrir qué secuencias de comandos descargar. Esto oculta tus secuencias de comandos de los escáneres de precarga. Los navegadores usan estos escáneres para descubrir recursos en páginas que es probable que visites a continuación o descubrir recursos de páginas mientras otro recurso bloquea el analizador.

Podemos volver a agregar la visibilidad si colocamos lo siguiente en el encabezado del documento:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Esto le indica al navegador que la página necesita las versiones 1.js y 2.js. link[rel=subresource] es similar a link[rel=prefetch], pero con semántica diferente. Lamentablemente, en este momento solo se admite en Chrome, por lo que debes declarar qué secuencias de comandos quieres cargar dos veces, una mediante los elementos de vínculos y otra vez en la secuencia de comandos.

Corrección: Originalmente, dije que los recogía el escáner de precarga, no lo son; el analizador normal los recoge. Sin embargo, el escáner de precarga podría detectarlos, pero aún no lo hace, mientras que las secuencias de comandos incluidas por el código ejecutable nunca pueden precargarse. Gracias a Yoav Weiss, que me corrigió en los comentarios.

Este artículo me parece deprimente

La situación es deprimente y deberías sentirte deprimido. No hay una forma no repetitiva, pero declarativa, de descargar secuencias de comandos de forma rápida y asíncrona mientras se controla el orden de ejecución. Con HTTP2/SPDY puedes reducir la sobrecarga de solicitudes al punto en que la forma más rápida es entregar secuencias de comandos en varios archivos pequeños que se pueden almacenar en caché de forma individual. Imagina lo siguiente:

<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>

Cada secuencia de comandos de mejoras se ocupa de un componente de página en particular, pero requiere funciones de utilidad en depends.js. Lo ideal es descargar todo de forma asíncrona y, luego, ejecutar las secuencias de comandos de mejoras lo antes posible y en cualquier orden, pero después de depends.js. ¡Es una mejora progresiva! Por desgracia, no hay una forma declarativa de lograr esto, a menos que las secuencias de comandos en sí se modifiquen para realizar un seguimiento del estado de carga de depends.js. Incluso async=false no soluciona este problema, ya que la ejecución de mejoras-10.js se bloqueará en 1-9. De hecho, solo hay un navegador que lo hace posible sin hackeos...

¡IE tiene una idea!

IE carga las secuencias de comandos de manera diferente en otros navegadores.

var script = document.createElement('script');
script.src = 'whatever.js';

IE comenzará a descargar "whatever.js" ahora; los demás navegadores no comenzarán a descargar hasta que la secuencia de comandos se haya agregado al documento. IE también tiene un evento, "readystatechange", y la propiedad "readystate", que nos indican el progreso de la carga. Esto es muy útil, ya que nos permite controlar la carga y la ejecución de las secuencias de comandos de forma independiente.

var script = document.createElement('script');

script.onreadystatechange = function() {
  if (script.readyState == 'loaded') {
    // Our script has download, but hasn't executed.
    // It won't execute until we do:
    document.body.appendChild(script);
  }
};

script.src = 'whatever.js';

Podemos crear modelos de dependencia complejos eligiendo cuándo agregar secuencias de comandos al documento. IE es compatible con este modelo desde la versión 6. Es muy interesante, pero sigue teniendo el mismo problema de visibilidad del precargador que async=false.

¡Suficiente! ¿Cómo debo cargar las secuencias de comandos?

Muy bien. Si deseas cargar secuencias de comandos de una manera que no bloquee la renderización, que no implique repetición y tenga una excelente compatibilidad con los navegadores, te propongo lo siguiente:

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Eso. Al final del elemento body. Sí, ser desarrollador web es como ser el Rey Sisifus. 100 puntos hípster como referencia en la mitología griega). Las limitaciones en HTML y en los navegadores nos impiden mejorar mucho.

Espero que los módulos de JavaScript nos ahorren al proporcionar una forma declarativa y sin bloqueo de cargar secuencias de comandos y dar control sobre el orden de ejecución, aunque esto requiere que las secuencias de comandos se escriban como módulos.

¿Debemos haber algo mejor que podamos usar ahora?

Si desea tomar medidas muy agresivas con respecto al rendimiento y no le importa un poco la complejidad y la repetición, pueden combinar algunos de los trucos anteriores, pero bastante justo para obtener puntos adicionales.

En primer lugar, agregamos la declaración del subrecurso para los precargadores:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Luego, intercalados en el encabezado del documento, cargamos nuestras secuencias de comandos con JavaScript, a través de async=false, regresando a la carga de secuencias de comandos basada en estado listo de IE y recurriendo a la aplazamiento.

var scripts = [
  '1.js',
  '2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];

// Watch scripts load in IE
function stateChange() {
  // Execute as many scripts in order as we can
  var pendingScript;
  while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
    pendingScript = pendingScripts.shift();
    // avoid future loading events from this script (eg, if src changes)
    pendingScript.onreadystatechange = null;
    // can't just appendChild, old IE bug if element isn't closed
    firstScript.parentNode.insertBefore(pendingScript, firstScript);
  }
}

// loop through our script urls
while (src = scripts.shift()) {
  if ('async' in firstScript) { // modern browsers
    script = document.createElement('script');
    script.async = false;
    script.src = src;
    document.head.appendChild(script);
  }
  else if (firstScript.readyState) { // IE<10
    // create a script and add it to our todo pile
    script = document.createElement('script');
    pendingScripts.push(script);
    // listen for state changes
    script.onreadystatechange = stateChange;
    // must set src AFTER adding onreadystatechange listener
    // else we'll miss the loaded event for cached scripts
    script.src = src;
  }
  else { // fall back to defer
    document.write('<script src="' + src + '" defer></'+'script>');
  }
}

Algunos trucos y una reducción más adelante, son 362 bytes + las URLs de tu secuencia de comandos:

!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
  "//other-domain.com/1.js",
  "2.js"
])

¿Vale la pena los bytes adicionales en comparación con una simple secuencia de comandos? Si ya usas JavaScript para cargar secuencias de comandos condicionalmente, como lo hace la BBC, también podrías beneficiarte de activar esas descargas antes. De lo contrario, tal vez no, quédate con el método simple del final del cuerpo.

¡Uf! Ahora sé por qué la sección de carga de secuencias de comandos de WHG es tan amplia. Necesito un trago.

Referencia rápida

Elementos de secuencia de comandos sin formato

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Las especificaciones indican lo siguiente: Se descargan juntas, se ejecutan en orden después de cualquier CSS pendiente y se bloquea la renderización hasta que se complete. Los navegadores dicen: ¡Sí, señor!

Aplazar

<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>

La especificación indica: Descargar juntos, ejecutar en orden justo antes de DOMContentLoaded. Ignora "defer" en las secuencias de comandos sin "src". IE < 10 dice: Podría ejecutar 2.js a mitad de la ejecución de 1.js. ¿No es divertido? Los navegadores en rojo dicen: No tengo idea de qué es esto “diferir”, voy a cargar las secuencias de comandos como si no estuvieran allí. Otros navegadores indican lo siguiente: Está bien, pero es posible que no ignore "defer" en las secuencias de comandos sin "src".

Modo asíncrono

<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>

Indica la especificación: Descarguen juntos y ejecútenlo en el orden en que se descargaron. Los navegadores en rojo dicen: ¿Qué significa “asíncrona”? Voy a cargar las secuencias de comandos como si no estuvieran allí. Otros navegadores dicen: Sí, está bien.

Async false

[
  '1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

Las especificaciones indican lo siguiente: Se deben descargar juntos y ejecutar en orden en cuanto se descarguen todas. Firefox < 3.6, Opera dice: No tengo idea de qué es esto "asíncrona", pero ocurre lo mismo, pero ejecuto secuencias de comandos agregadas a través de JS en el orden en que se agregaron. Safari 5.0 dice: Comprendo la condición “async”, pero no entiendo establecerlo en “false” con JS. Ejecutaré sus secuencias de comandos en cuanto lleguen, en cualquier orden. IE < 10 dice: No tengo idea sobre "async", pero hay una solución alternativa para "onreadystatechange". Otros navegadores en rojo dicen: No entiendo lo que es "asíncrona", pero ejecutaré tus secuencias de comandos en cuanto lleguen, en cualquier orden. Todo lo demás dice: Soy tu amigo, lo haremos junto al libro.