How do I force JavaScript to load in sequential order?



I’m struggling with loading JS files in sequential order, despite having looked at various other SO posts and documentation on async and defer. My code structure is as follows:

<script src="lang.js"></script> <!--dynamically loads either eng.js or fra.js-->

<script src="task1.js"></script> <!--all of the task*.js depend on eng.js/fra.js-->
<script src="task2.js"></script>
<script src="task3.js"></script>
<script src="task4.js"></script>

<script>
   // inline JS; depends on all task*.js
</script>

The contents of lang.js are as follows:

let langScript = document.createElement("script")
// FR is a boolean defined earlier
langScript.setAttribute("src", FR ? "fra.js" : "eng.js"); 
langScript.setAttribute("async", "false");

let head = document.head;
head.insertBefore(langScript, head.firstElementChild);

By my understanding, these scripts should load and execute in the order eng.js/fra.js -> task*.js -> inline, but this doesn’t seem to be the case (at least in Chrome and Firefox). How should I modify my code such that it executes in the correct order? (I would prefer not to use callbacks if possible as I don’t want to change the individual JS files too much.)

Answer

Without using import()

You could do something like the following:

  • Remove the <script> elements for each of the task*.js files from the document
  • Add an event listener for the load event of the inserted language script
  • Create each of the task*.js scripts inside that event listener
  • Add event listeners for the load event of each task*.js and use them to resolve a Promise, which is combined to form a global Promise
  • Wait on that global Promise in the inline script.

Doing that, the relevant parts of lang.js would become:

const langScript = document.createElement('script');
langScript.setAttribute('src', FR ? 'fra.js' : 'eng.js');

const fullyLoaded = new Promise(resolve => {
  langScript.addEventListener('load', () => {
    const taskPromises = [];
    for (let i = 1; i < 5; i++) {
      const script = document.createElement('script');
      script.setAttribute('src', `task${i}.js`);
      taskPromises.push(new Promise(resolve => {
        script.addEventListener('load', resolve);
      }));
      head.insertBefore(script, head.firstElementChild);
    }
    resolve(Promise.all(taskPromises));
  });
});

const head = document.head;
head.insertBefore(langScript, head.firstElementChild);

and the document would look something like:

<html>
  <head>
    <script src="lang.js"></script>
    <script>
      window.addEventListener('load', async () => {
        await fullyLoaded;

        console.log('start of inline');
      });
    </script>
  </head>
</html>

None of the other scripts would need to be modified.

With this scheme:

  • lang.js is loaded first
  • eng.js/fra.js is completely loaded second
  • task1.js through task4.js are completely loaded in any order
  • inline scripts are run last

You will need to look at whether this manual deferral causes the loading to take to long; mocking this up locally has all the scripts loaded anywhere from 150ms to 450ms.

Using import()

Effectively the same as the above, but using the import() function-like keyword, lang.js becomes:

const src = FR ? './fra.js' : './eng.js';

const fullyLoaded = import(src).then(() => Promise.all([
  import('./task1.js'),
  import('./task2.js'),
  import('./task3.js'),
  import('./task4.js')
]));

There are some differences in how JavaScript code is run inside something that is imported like this. The big ones are the imposition of strict mode, and the isolation of context, so you will most likely need to store any global variables explicitly onto the window variable, if you aren’t already, for the eng.js, fra.js and task*.js files.



Source: stackoverflow