Load ordering of dynamically added script tags
Asked Answered
E

4

13

I have a typescript application that dynamically adds script tags that point to JS files. Due to some restrictions I cant have these script tags statically defined in a html file, so I add them dynamically through typescript like this:

for (let src of jsFiles) {
  let sTag = document.createElement('script');
  sTag.type = 'text/javascript';
  sTag.src = src;
  sTag.defer = true;
  document.body.appendChild(script);
}

Now, I am noticing that, when I add script tags dynamically, their doesn't seem to be a guarantee in the order in which they are loaded. Unfortunately, the jsFiles array has scripts that are dependent on each other. So, the 2nd script in the array can only be loaded after the first one is fully loaded. The second script references a function that is defined in the first one. Is there a way I can specify the order in which scripts are ordered and executed when adding them dynamically (similar to how ordering is done when you statically define script tags in a html file)?

P.S. I would like to avoid using the onload callback to solve this issue since I noticed a performance degradation with my application. My second script file is very large and I am assuming that caused the degradation.

Ever answered 8/8, 2016 at 22:50 Comment(2)
In plain old JS, I'd solve this by using RequireJS to make dependencies explicit. Then each script's main code is always wrapped, and they always execute in the order I want, no matter the overlying execution order. A similar approach could work for you. If your project is small, and it will always be small, you can add a non-deferred script at the beginning to manage the execution order. Then any scripts that need to delay their execution can get wrapped in a function which is passed into this controlling script.Vermiculate
This answer has some helpful commentary on the defer attribute.Vermiculate
C
22

I can mention some alternatives to overcome that requirement:

  1. Use a library to inject dependencies (AMD or CommonJS modules)
    Just use modules. ES2015: import/export, or CommonJS: require().
  2. Create the script tag programmatically and set the callback onload in order to react when the script has been loaded asynchronously. The attribute async = true is set by default.
  3. If you are allowed to modify the scripts to inject, then add a line at the end of scripts with an object or array that keeps track of the scripts already loaded.
  4. You can fetch the scripts as text (XMLHttpRequest), then, build a string with the scripts in the required order and, finally, execute the text-script via eval()
  5. And the less recommended option but frequently used, set a setInterval to check if the script was already executed.

I recommend going for the first option. But for academic purposes, I'm going to illustrate the second option:

Create the script tag programmatically and set the callback onload in order to react when the script has been loaded asynchronously.

I want to recommend a reading about script loaders: Deep dive into the murky waters of script loading, half hour worth spending!

The following example is a small module to manage scripts injection, and this is the basic idea behind it:

let _scriptsToLoad = [
  'path/to/script1.js',
  'path/to/script2.js',
  'path/to/script3.js'
];

function createScriptElement() {
  // gets the first script in the list
  let script = _scriptsToLoad.shift();
  // all scripts were loaded
  if (!script) return;
  let js = document.createElement('script');
  js.type = 'text/javascript';
  js.src = script;
  js.onload = onScriptLoaded;
  let s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(js, s);
}

function onScriptLoaded(event) {
  // loads the next script
  createScriptElement();
};

In this plunker you can test the injection of scripts asynchronously in a specific order:

The main idea was to create an API that allows you interact with the scripts to inject, by exposing the following methods:

  • addScript: receive an URL or a list of URLs for each script to be loaded.
  • load: Run the task to load scripts in the specified order.
  • reset: Clear the array of scripts, or cancels the load of scripts.
  • afterLoad: Callback executed after every script has been loaded.
  • onComplete: Callback executed after all scripts have been loaded.

I like Fluent Interface or method chaining technique, so I built the module that way:

  scriptsLoader
    .reset()
    .addScript("script1.js")
    .addScript(["script2.js", "script3.js"])
    .afterLoad((src) => console.warn("> loaded from jsuLoader:", src))
    .onComplete(() => console.info("* ALL SCRIPTS LOADED *"))
    .load();

In the code above, we load first the "script1.js" file, and execute the afterLoad() callback, next, do the same with "script2.js" and "script3.js" and after all scripts has been loaded, the onComplete() callback is executed.

Cupp answered 9/8, 2016 at 1:33 Comment(3)
Is this still a valid approach in 2019 or is there a better approach? The article that was referenced is from 2013 which is ancient in terms of the internet.Plutocrat
Hi @JoAMoS, No, this solution is for old projects running only in the browser. For projects running under TypeScript, Webpack, RollUp and others, use ES6 Imports, or commonjs syntax. See: ES importsCupp
Sill helpful a couple years later where I can't decide at buildtime the dependencies (done runtime). Thks.Redd
U
2

I would like to avoid using the onload callback to solve this issue since I noticed a performance degradation with my application.

If you want the files ordered ... you need to wait for onload on each file. No way around it.

Here is a utility function I wrote once:

/**
 * Utility : Resolves when a script has been loaded
 */
function include(url: string) {
  return new Promise<{ script: HTMLScriptElement }>((resolve, reject) => {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = url;

    script.onload = function() {
      resolve({ script });
    };

    document.getElementsByTagName('head')[0].appendChild(script);
  });
}
Uncinariasis answered 9/8, 2016 at 3:52 Comment(3)
I believe there are libraries that allow you to download the files asynchronously but then execute them in order. See RequireJS etc. They will perform much better than waiting for onload to even start the download of the next script.Knox
Recommending RequireJS in mid-2017 was not a great suggestion. RequireJS is outdated and not supported anymore - has been for a few years. Check out Webpack, JSPM and Rollup.js as better alternatives.Cramoisy
or script.js and loadjs github.com/ded/script.js github.com/muicss/loadjsDistort
F
1

For anyone who is using jQuery I have improved @adeneo script so it will load all scripts in the specified order. It doesn't do chain loading, so it's very fast, but if you want even faster, change the 50 ms wait time.

$.getMultiScripts = function(arr, path) {

    function executeInOrder(scr, code, resolve) {
        // if its the first script that should be executed
        if (scr == arr[0]) {
            arr.shift();
            eval(code);
            resolve();
            console.log('executed', scr);
        } else {
            // waiting
            setTimeout(function(){
                executeInOrder(scr, code, resolve);
            }, 50);
        }
    }

    var _arr = $.map(arr, function(scr) {

        return new Promise((resolve) => {
            jQuery.ajax({
                type: "GET",
                url: (path || '') + scr,
                dataType: "text",
                success: function(code) {
                    console.log('loaded  ', scr);
                    executeInOrder(scr, code, resolve);
                },
                cache: true
            });
        });

    });
        
    _arr.push($.Deferred(function( deferred ){
        $( deferred.resolve );
    }));
        
    return $.when.apply($, _arr);
}

How to use:

var script_arr = [
    'myscript1.js', 
    'myscript2.js', 
    'myscript3.js'
];

$.getMultiScripts(script_arr, '/mypath/').done(function() {
    // all scripts loaded
});
Feer answered 5/11, 2020 at 15:43 Comment(0)
W
1

According to this Medium article:

Scripts that are dynamically created and added to the document are async by default, which means they are not guaranteed to execute in the same order they are added. However, we can change this behavior by explicitly marking them as not async

So, adding sTag.async = false ought to load the scripts in the order in which they were added. It appeared to work for me when testing with Chrome and Firefox, but I haven't tested other browsers.

Windswept answered 22/12, 2023 at 3:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.