Techumber
Home Blog Work

Simple Non-Blocking JavaScript Loading

Published on May 15, 2014

A web browser is a collection of software engines (parse, render, js engines like v8, so on…). When we open a web page in our browsers the parser takes control and start analyzing the HTML tags. When it reaches to the <script> tag with an src attribute in the current HTML document flow it stops the rendering and wait for the specific script to download. Once script download did then it continues with the page rendering. This is known as blocking.

In case if the script is not there in given location I will wait until it reaches to its maximum connection time and give us 404 error. So in order to create non-blocking JavaScript loading, we can use new html5 async and IE5 and above defer attributes to avoid the blocking problem. If we use this attributes on our <script> tag the render engine won’t stop the execution of downloading script. But these two have their own problems.

The Best Solution

The perfect solution to this problem is creating script tag on a fly and attaching an src attribute at run time. Since creating script tag via JavaScript is JavaScript engine’s (in chrome v8 engine) task so it won’t interrupt parser engine. So render engine can render your page faster and the page performance will be improved.

Simple Script

(function() {
  var s = document.createElement("script");
  s.src = "path/to/script.js";
  s.type = "text/javascript";
  (document.body || document.head[0]).appendChild(s);
})();

In above code, we creating a script tag using DOM methods and appending it to the body tag if it is available or we will append it to the head tag. Note: Here I put document.head[0] instead of document.head to fix IE bug. Now, let’s consider we have function hi in script.js file and if we try to call that function function immediately after above code it may not work, the reason it will some time to download the script so when we call the hi function you will get undefined.

Detecting script load completed

So in order to solve above problem, we need an event that detects if script download complete and we must have a callback. The callback must be executed when the script download is completed. The simplest solution is as below.

function loadJS(url, callback) {
  var s = document.createElement("script");
  s.type = "text/javascript";
  s.onload = function() {
    callback();
  };
  s.src = url;
  (document.body || document.head[0]).appendChild(s);
}

This will work in most of the browsers but our friend IE NAA!. It is always different. So in order to make our code work in all browsers change the above function as.

function loadJS(url, callback) {
  var s = document.createElement("script");
  s.type = "text/javascript";
  if (script.readyState) {
    //IE
    script.onreadystatechange = function() {
      if (script.readyState == "loaded" || script.readyState == "complete") {
        script.onreadystatechange = null;
        callback();
      }
    };
  } else {
    //Others
    script.onload = function() {
      callback();
    };
  }
  s.src = url;
  (document.body || document.head[0]).appendChild(s);
}

This function will work for only one script. What if we have multiple scripts and one callback? We want to execute our callback function once all our scripts downloaded and appended to the document.

Multiple Scripts Loading

function loadJS(urls, callback) {
  var i,
    assert = [],
    loadedScipts = 0;
  //loop throught urls array
  for (i = 0; i < urls.length; i++) {
    assert.push(urls[i]);
    load(urls[i], callback);
  }
  //Single script loading
  function load(url) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    if (script.readyState) {
      //IE
      script.onreadystatechange = function() {
        if (script.readyState == "loaded" || script.readyState == "complete") {
          script.onreadystatechange = null;
          loadedScipts++;
        }
      };
    } else {
      //Others
      script.onload = function() {
        loadedScipts++;
      };
    }
    script.src = url;
    (document.body || document.head[0]).appendChild(script);
  }
  // to check if all scripts loaded or not
  function isAllLoaded() {
    if (loadedScripts === urls.length) {
      return true;
    }
    return false;
  }
  //If callback exists
  if (callback) {
    setTimeout(function() {
      if (isAllLoaded) {
        callback();
        clearTimeout(this);
      }
    }, 50);
  }
}

In above code, we moving all our previous code into load function within loadJS function. We will loop through all URLs array and call load function to load JavaScript files. The counter loadedScripts which tracks how many scripts loaded until now. By using isAllLoaded function we know if all scripts are loaded or not. Finally, we call callback function to if all scripts are loaded. The setTimeOut is useful to check if all scripts loaded or not in every 50ms interval time.

DOM Ready

There is a debate on the perfect place to put JavaScript file in HTML document. Few developer in favor of putting inside head and few just before closing body tag. I personally feel the second one is the best option so that we can ensure the entire page is downloaded and if there is any DOM manipulation need to be done then we don’t need to worry if the node is available or not. So now, let’s create simple DOM Ready function.

// Check if dom is ready
function DOMReady(callback) {
  if (document.addEventListener) {
    // native event
    document.addEventListener("DOMContentLoaded", callback, false);
  } else if (window.addEventListener) {
    window.addEventListener("load", callback, false);
  } else if (document.attachEvent) {
    window.attachEvent("onload", callback);
  }
}

DOMContentLoaded is a native event which is support by most of the modern browsers. For back browsers compatibility we are using load event. Now, we can remove the condition document.head[0] form above loadJS function.

Complete Code

function loadJS(urls, callback) {
  var i,
    assert = [],
    loadedScipts = 0;
  //loop throught urls array
  for (i = 0; i < urls.length; i++) {
    assert.push(urls[i]);
    load(urls[i], callback);
  }
  //Single script loading
  function load(url) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    if (script.readyState) {
      //IE
      script.onreadystatechange = function() {
        if (script.readyState == "loaded" || script.readyState == "complete") {
          script.onreadystatechange = null;
          loadedScipts++;
        }
      };
    } else {
      //Others
      script.onload = function() {
        loadedScipts++;
      };
    }
    script.src = url;
    document.body.appendChild(script);
  }
  // to check if all scripts loaded or not
  function isAllLoaded() {
    if (loadedScripts === urls.length) {
      return true;
    }
    return false;
  }
  //If callback exists
  if (callback) {
    setTimeout(function() {
      if (isAllLoaded) {
        callback();
        clearTimeout(this);
      }
    }, 50);
  }
}

// Check if dom is ready
function DOMReady(callback) {
  if (document.addEventListener) {
    // native event
    document.addEventListener("DOMContentLoaded", callback, false);
  } else if (window.addEventListener) {
    window.addEventListener("load", callback, false);
  } else if (document.attachEvent) {
    window.attachEvent("onload", callback);
  }
}

How to Use it

Now, let’s see how to use our code to load scripts in the html document.

DOMReady(function() {
  loadJS(
    ["path/to/script1.js", "path/to/scripts2.js", "path/to/scripts2.js"],
    function() {
      alert("All Scripts Loaded");
    }
  );
});

That’s it hopes you enjoyed!!!!!