HiveBrain v1.2.0
Get Started
← Back to all entries
principlejavascriptMinor

Is this a good approach to loading JavaScript files asynchronously?

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
thisjavascriptfilesloadingasynchronouslygoodapproach

Problem

I want to load JS files asynchronously to speed up page loading, but also need to execute JS code only once the scripts finish loading (so I need a way to implement callbacks).

I based this off of some example code on the Google Pagespeed website:

// Add script elements as a children of the body
function downloadJSAtOnload() {
    var filesToLoad = [
        ["/assets/plugins/typeahead.bundle.min.js", "onTypeaheadLoaded"],
        ["https://apis.google.com/js/client:plusone.js?onload=googlePlusOnloadCallback", ""]
    ];

    filesToLoad.forEach(function(entry) {
        var element = document.createElement("script");
        element.src = entry[0];
        if (entry[1] != "") { // if an onload callback is present (NOTE: DOES NOT SUPPORT NAMESPACES -- http://stackoverflow.com/a/359910/1101095)
            element.onload = function() {
                if (typeof window[entry[1]] != "undefined") {
                    window[entry[1]]();
                }
            };
        }
        document.body.appendChild(element);
    });
}

// Check for browser support of event handling capability
if (window.addEventListener)
    window.addEventListener("load", downloadJSAtOnload, false);
else if (window.attachEvent)
    window.attachEvent("onload", downloadJSAtOnload);
else window.onload = downloadJSAtOnload;


I know there are libraries out there for dependency management, callback functions, etc., but I just want something very lightweight and using a library would mean having another HTTP request.

I'm very new to JavaScript and would really appreciate any feedback or advice you might have!

Solution

First of all, I would split you problem into 3 parts:

  • Loading a js-file asynchronously, and calling a callback-function



  • Loading several js-files an once asynchronously



  • Using an onload-Handler, which triggers everything



These are 3 independent parts, which might be reused for other purposes, if seperated correctly.

  1. Loading a js-file asynchronously



You're using 2 strings to specify a file to be loaded. But instead of writing

["file.js", "cbFunction"]


you can as well also pass the actual function:

["file.js", cbFunction]


This not only ensures that the function actually exists, it also gives you the opportunity to use anonymous functions:

["file.js", function(){ alert("loaded!"); } ]


However, speaking of segregation of responsibilities, you should write a function on its own, without the need of an Array anymore:

function loadScript(path, scriptLoadedCallback){
    var element=document.createElement('script');    //generate -tag
    element.setAttribute("type","text/javascript");
    element.setAttribute("src", path);

    if(typeof(scriptLoadedCallback) == 'function'){  //makes the callback-function optional
        element.onload = function() {
            return scriptLoadedCallback(true, path); //true = successfull; the path is needed later
        };
        element.onerror = function() {               //you might also call the cb on error
            this.parentNode.removeChild(this);       //remove faulty node from DOM
            return scriptLoadedCallback(false, path);      //false = error; the path is needed later
        };
    }
    document.head.appendChild(fileref);              //insert the node in DOM (end of ), and load the script
}


This function looks much clearer, and you can see at first glance what it is doing.

Note that I checked the existence of the callback before I append an onload-attribute. This is a bit more efficient than your code, which always calls the onload-Handler and then does nothing.

You might also want to add the onerror-method, otherwise you will never get notified if the loading failed (This usually happens if the js-file doesn't exist).

If you only have to load a single script, you can call this function directly. No need to generate useless Arrays.

  1. Loading several js-files an once



If you only want to load several files, and calling a callback after each file, this is an easy task, using your original array (with functions instead of strings):

var filesToLoad = [
    ["file1.js", cbFunctionToCall],
    ["file2.js"]
];
for(var i=0; i<filesToLoad.length; i++){ //I prefer a for-loop because its more readable
    loadScript(filesToLoad[i][0], filesToLoad[i][1];
}


And that's all.

However, if you want to have one callback-function which is called after all files have been loaded, this gets a bit tricky. You don't know which file is the last one, and which file should trigger the actual callback-function.

function loadScriptArray(contentArray, contentLoadedCallback){
    var contentQuantity = contentArray.length;      //Number of Files that needs to be loaded
    var contentCompleted = 0;                       //Number of Files, that have already been loaded
    var returnParamList = {};                       //List with return-Parameters

    if(contentQuantity == 0){                       //We don't have anything to load
        return contentLoadedCallback({});
    }

    for (var i = 0; i < contentQuantity; i++) {
        loadScript(contentArray[i], function(success, path){   //This anonymous function is called everytime a script is finished
                                                               //The only way to know which script finished, is to pass the path as an parameter
            returnParamList[ identifier ] = returnParam;       //store the returnValue (true=success, false=error)
            contentCompleted++;
            if(contentCompleted == contentQuantity){    //this was the last file
                if(typeof contentLoadedCallback== 'function'){
                    contentLoadedCallback(returnParamList);
                }
            }
        });
    }
}


This is how the function is being called:

loadScriptArray(["file1.js", "file2.js"], function(returnParamList){
    alert("All Scripts finished. \n"+
          "File1: "+returnParamList["file1.js"]+"\n"+
          "File2: "+returnParamList["file2.js"]);
});


This function loads 2 files simultaneously, and calls the callback after the last one has been loaded. Afterwards it prints which files have been loaded successfully, and which loading-operation failed.

This only works, if the initial loadScript()-Function always calls the callback-Function - both if it succeeded or failed. And to report the success back to the caller, the callback-function also needs the filePath as an parameter.

Another advantage is, that you can extend this function to also load other files like images, json, xml and so on.

Code Snippets

["file.js", "cbFunction"]
["file.js", cbFunction]
["file.js", function(){ alert("loaded!"); } ]
function loadScript(path, scriptLoadedCallback){
    var element=document.createElement('script');    //generate <script>-tag
    element.setAttribute("type","text/javascript");
    element.setAttribute("src", path);

    if(typeof(scriptLoadedCallback) == 'function'){  //makes the callback-function optional
        element.onload = function() {
            return scriptLoadedCallback(true, path); //true = successfull; the path is needed later
        };
        element.onerror = function() {               //you might also call the cb on error
            this.parentNode.removeChild(this);       //remove faulty node from DOM
            return scriptLoadedCallback(false, path);      //false = error; the path is needed later
        };
    }
    document.head.appendChild(fileref);              //insert the node in DOM (end of <head>), and load the script
}
var filesToLoad = [
    ["file1.js", cbFunctionToCall],
    ["file2.js"]
];
for(var i=0; i<filesToLoad.length; i++){ //I prefer a for-loop because its more readable
    loadScript(filesToLoad[i][0], filesToLoad[i][1];
}

Context

StackExchange Code Review Q#57458, answer score: 5

Revisions (0)

No revisions yet.