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!!!!!