cargar y ejecutar el orden de los scripts

265

Hay muchas maneras diferentes de incluir JavaScript en una página html. Conozco las siguientes opciones:

  • código en línea o cargado desde URI externo
  • incluido en la etiqueta <head> o <body> [ 1 , 2 ]
  • sin ninguno defero asyncatributo (solo scripts externos)
  • incluido en fuente estática o agregado dinámicamente por otros scripts (en diferentes estados de análisis, con diferentes métodos)

Sin contar las onEventsecuencias de comandos del navegador desde el disco duro, javascript: URI y -atributos [ 3 ], ya hay 16 alternativas para ejecutar JS y estoy seguro de que olvidé algo.

No estoy tan preocupado por la carga rápida (paralela), tengo más curiosidad sobre el orden de ejecución (que puede depender del orden de carga y el orden del documento ). ¿Existe una buena referencia (navegador cruzado) que cubra realmente todos los casos? Por ejemplo, http://www.websiteoptimization.com/speed/tweak/defer/ solo trata con 6 de ellos y prueba principalmente navegadores antiguos.

Como me temo que no, aquí está mi pregunta específica: tengo algunos guiones de cabeza (externos) para la inicialización y la carga de guiones. Luego tengo dos scripts estáticos en línea al final del cuerpo. El primero permite que el cargador de scripts agregue dinámicamente otro elemento de scripts (haciendo referencia a js externos) al cuerpo. El segundo de los scripts estáticos en línea quiere usar js del script externo agregado. ¿Puede confiar en que el otro haya sido ejecutado (y por qué :-)?

Bergi
fuente
¿Has mirado Cargando guiones sin bloqueo de Steve Souders? Ahora está un poco anticuado, pero aún contiene algunas ideas valiosas sobre el comportamiento del navegador dada una técnica de carga de script específica.
Josh Habdas

Respuestas:

331

Si no carga dinámicamente los scripts o los marca como defero async, los scripts se cargan en el orden que se encuentran en la página. No importa si se trata de un script externo o un script en línea: se ejecutan en el orden en que se encuentran en la página. Los scripts en línea que vienen después de los scripts externos se mantienen hasta que todos los scripts externos que vinieron antes que ellos se hayan cargado y ejecutado.

Las secuencias de comandos asíncronas (independientemente de cómo se especifiquen como asíncronas) se cargan y ejecutan en un orden impredecible. El navegador los carga en paralelo y es libre de ejecutarlos en el orden que desee.

No hay un orden predecible entre varias cosas asíncronas. Si se necesitara un orden predecible, entonces tendría que codificarse al registrarse para recibir notificaciones de carga de los scripts asíncronos y secuenciar manualmente las llamadas de JavaScript cuando se cargan las cosas apropiadas.

Cuando una etiqueta de script se inserta dinámicamente, el comportamiento del orden de ejecución dependerá del navegador. Puedes ver cómo se comporta Firefox en este artículo de referencia . En pocas palabras, las versiones más recientes de Firefox predeterminan una etiqueta de secuencia de comandos agregada dinámicamente a asíncrono a menos que la etiqueta de secuencia de comandos se haya establecido de otra manera.

Una etiqueta de script con asyncpuede ejecutarse tan pronto como se cargue. De hecho, el navegador puede pausar el analizador de cualquier otra cosa que esté haciendo y ejecutar ese script. Por lo tanto, realmente puede ejecutarse en casi cualquier momento. Si el script se almacenó en caché, podría ejecutarse casi de inmediato. Si el script tarda un tiempo en cargarse, podría ejecutarse después de que se complete el analizador. Una cosa para recordar asynces que puede ejecutarse en cualquier momento y que el tiempo no es predecible.

Una etiqueta de secuencia de comandos deferespera hasta que se complete el analizador completo y luego ejecuta todas las secuencias de comandos marcadas deferen el orden en que se encontraron. Esto le permite marcar varios scripts que dependen unos de otros como defer. Todos se pospondrán hasta que finalice el analizador de documentos, pero se ejecutarán en el orden en que se encontraron preservando sus dependencias. Pienso que deferlos scripts se colocan en una cola que se procesará después de que se complete el analizador. Técnicamente, el navegador puede estar descargando los scripts en segundo plano en cualquier momento, pero no ejecutarán ni bloquearán el analizador hasta que el analizador haya terminado de analizar la página y analice y ejecute los scripts en línea que no están marcados defero async.

Aquí hay una cita de ese artículo:

los scripts insertados con script se ejecutan de forma asincrónica en IE y WebKit, pero sincrónicamente en Opera y Firefox anterior a 4.0.

La parte relevante de la especificación HTML5 (para navegadores compatibles más nuevos) está aquí . Hay mucho escrito allí sobre el comportamiento asíncrono. Obviamente, esta especificación no se aplica a los navegadores más antiguos (o navegadores mal conformes) cuyo comportamiento probablemente tendría que probar para determinar.

Una cita de la especificación HTML5:

Luego, se debe seguir la primera de las siguientes opciones que describe la situación:

Si el elemento tiene un atributo src, y el elemento tiene un atributo diferido, y el elemento se ha marcado como "insertado en el analizador sintáctico", y el elemento no tiene un atributo asíncrono. El elemento debe agregarse al final de la lista de scripts que se ejecutarán cuando el documento haya finalizado el análisis asociado con el Documento del analizador que creó el elemento.

La tarea que la fuente de la tarea de red coloca en la cola de tareas una vez que el algoritmo de recuperación se ha completado debe establecer el indicador "listo para ser ejecutado por el analizador" del elemento. El analizador se encargará de ejecutar el script.

Si el elemento tiene un atributo src, y el elemento se ha marcado como "insertado en el analizador", y el elemento no tiene un atributo asíncrono. El elemento es el script de bloqueo de análisis pendiente del documento del analizador que lo creó. (Solo puede haber una secuencia de comandos por documento a la vez).

La tarea que la fuente de la tarea de red coloca en la cola de tareas una vez que el algoritmo de recuperación se ha completado debe establecer el indicador "listo para ser ejecutado por el analizador" del elemento. El analizador se encargará de ejecutar el script.

Si el elemento no tiene un atributo src, y el elemento se ha marcado como "insertado en el analizador sintáctico", y el Documento del analizador HTML o analizador XML que creó el elemento de secuencia de comandos tiene una hoja de estilo que bloquea las secuencias de comandos. El elemento es el script de bloqueo de análisis pendiente del documento del analizador que creó el elemento. (Solo puede haber una secuencia de comandos por documento a la vez).

Establezca el indicador "listo para ser ejecutado por el analizador" del elemento. El analizador se encargará de ejecutar el script.

Si el elemento tiene un atributo src, no tiene un atributo asíncrono y no tiene establecido el indicador "force-async". El elemento debe agregarse al final de la lista de scripts que se ejecutarán en orden lo antes posible. con el documento del elemento de secuencia de comandos en el momento en que se inició la preparación de un algoritmo de secuencia de comandos.

La tarea que la fuente de la tarea de red coloca en la cola de tareas una vez que se ha completado el algoritmo de recuperación debe ejecutar los siguientes pasos:

Si el elemento no es ahora el primer elemento en la lista de scripts que se ejecutará en orden tan pronto como sea posible al que se agregó anteriormente, marque el elemento como listo pero cancele estos pasos sin ejecutar el script todavía.

Ejecución: Ejecute el bloque de secuencia de comandos correspondiente al primer elemento de secuencia de comandos en esta lista de secuencias de comandos que se ejecutará en orden lo antes posible.

Elimine el primer elemento de esta lista de scripts que se ejecutarán en orden lo antes posible.

Si esta lista de secuencias de comandos que se ejecutará en orden lo antes posible aún no está vacía y la primera entrada ya se ha marcado como lista, luego vuelva al paso etiquetado como ejecución.

Si el elemento tiene un atributo src El elemento debe agregarse al conjunto de scripts que se ejecutarán lo antes posible del Documento del elemento de script en el momento en que se inicia el algoritmo de preparación de un script.

La tarea que la fuente de la tarea de red coloca en la cola de tareas una vez que el algoritmo de recuperación se ha completado debe ejecutar el bloque de secuencia de comandos y luego eliminar el elemento del conjunto de secuencias de comandos que se ejecutará lo antes posible.

De lo contrario, el agente de usuario debe ejecutar inmediatamente el bloque de script, incluso si ya se están ejecutando otros scripts.


¿Qué pasa con los scripts del módulo Javascript type="module"?

Javascript ahora tiene soporte para cargar módulos con una sintaxis como esta:

<script type="module">
  import {addTextToBody} from './utils.mjs';

  addTextToBody('Modules are pretty cool.');
</script>

O, con srcatributo:

<script type="module" src="http://somedomain.com/somescript.mjs">
</script>

Todos los scripts con type="module"reciben automáticamente el deferatributo. Esto los descarga en paralelo (si no en línea) con otra carga de la página y luego los ejecuta en orden, pero después de que se realiza el analizador.

Los scripts del módulo también pueden recibir el asyncatributo que ejecutará los scripts del módulo en línea tan pronto como sea posible, sin esperar hasta que se complete el analizador y sin esperar a ejecutar el asyncscript en un orden particular en relación con otros scripts.

Hay un gráfico de línea de tiempo bastante útil que muestra la búsqueda y ejecución de diferentes combinaciones de scripts, incluidos los scripts de módulo aquí en este artículo: Carga del módulo Javascript .

jfriend00
fuente
Gracias por la respuesta, pero el problema es que el script se agrega dinámicamente a la página, lo que significa que se considera asíncrono . ¿O eso solo funciona en <head>? ¿Y mi experiencia también es que se ejecutan en orden de documentos?
Bergi el
@ Bergi: si se agrega dinámicamente, entonces es asíncrono y el orden de ejecución es indeterminado a menos que escriba código para controlarlo.
jfriend00
Simplemente, Kolink dice lo contrario ...
Bergi
@Bergi: OK, modifiqué mi respuesta para decir que los scripts asíncronos se cargan en un orden indeterminado. Se pueden cargar en cualquier orden. Si yo fuera tú, no contaría con que la observación de Kolink sea como siempre es. No conozco ningún estándar que diga que un script agregado dinámicamente tiene que ejecutarse de inmediato y tiene que bloquear otros scripts para que no se ejecuten hasta que se cargue. Esperaría que eso dependa del navegador y también de factores ambientales (si el script está en caché, etc.).
jfriend00
1
@RuudLenders: eso depende de la implementación del navegador. Al encontrar la etiqueta de secuencia de comandos anteriormente en el documento, pero marcada con deferle da al analizador la oportunidad de comenzar su descarga antes mientras pospone su ejecución. Tenga en cuenta que si tiene muchas secuencias de comandos del mismo host, comenzar la descarga más pronto en realidad puede ralentizar la descarga de otras personas desde el mismo host (ya que compiten por el ancho de banda) que su página está esperando (eso no es así defer) Esto podría ser una espada de doble filo.
jfriend00
13

El navegador ejecutará los scripts en el orden en que los encuentra. Si llama a un script externo, bloqueará la página hasta que el script se haya cargado y ejecutado.

Para probar este hecho:

// file: test.php
sleep(10);
die("alert('Done!');");

// HTML file:
<script type="text/javascript" src="test.php"></script>

Las secuencias de comandos agregadas dinámicamente se ejecutan tan pronto como se agregan al documento.

Para probar este hecho:

<!DOCTYPE HTML>
<html>
<head>
    <title>Test</title>
</head>
<body>
    <script type="text/javascript">
        var s = document.createElement('script');
        s.type = "text/javascript";
        s.src = "link.js"; // file contains alert("hello!");
        document.body.appendChild(s);
        alert("appended");
    </script>
    <script type="text/javascript">
        alert("final");
    </script>
</body>
</html>

El orden de las alertas se "agrega" -> "¡hola!" -> "final"

Si en un script intenta acceder a un elemento que aún no se ha alcanzado (ejemplo <script>do something with #blah</script><div id="blah"></div>:), recibirá un error.

En general, sí, puede incluir scripts externos y luego acceder a sus funciones y variables, pero solo si sale de la <script>etiqueta actual y comienza una nueva.

Niet the Dark Absol
fuente
Puedo confirmar ese comportamiento. Pero hay sugerencias en nuestras páginas de comentarios, que podrían funcionar solo cuando test.php está en caché. ¿Conoces algún enlace de especificación / referencia sobre esto?
Bergi el
44
link.js no está bloqueando. Use un script similar a su PHP para simular un tiempo de descarga prolongado.
1983
14
Esta respuesta es incorrecta. No siempre ocurre que "los scripts agregados dinámicamente se ejecutan tan pronto como se agregan al documento". A veces esto es cierto (por ejemplo, para versiones anteriores de Firefox), pero generalmente no lo es. La orden de ejecución, como se menciona en la respuesta de jfriend00, no está determinada.
Fabio Beltramini
1
No tiene sentido que los scripts se ejecuten en el orden en que aparecen en la página, independientemente de si están en línea o no. ¿Por qué entonces el fragmento del administrador de etiquetas de Google y muchos otros que he visto tienen código para insertar un nuevo script sobre todas las otras etiquetas de script en la página? ¿No tendría sentido hacer esto, si los scripts anteriores ya se han cargado seguramente? O me estoy perdiendo algo.
user3094826
2

Después de probar muchas opciones, descubrí que la siguiente solución simple es cargar los scripts cargados dinámicamente en el orden en que se agregan en todos los navegadores modernos

loadScripts(sources) {
    sources.forEach(src => {
        var script = document.createElement('script');
        script.src = src;
        script.async = false; //<-- the important part
        document.body.appendChild( script ); //<-- make sure to append to body instead of head 
    });
}

loadScripts(['/scr/script1.js','src/script2.js'])
Flion
fuente