timbenniks

front-end developer

Non-blocking JavaScript roundup

Non-blocking JavaScript has been a hot topic for a while now. I am going to try to explain why JavaScript blocks the browser while rendering a page and how you can solve this issue. This article is a roundup of the most commonly used techniques out there. Credits for these best practises go to the developers at Yahoo!.

About blocking

In my opinion JavaScript plays an important role in the perceived speed of a website. While JavaScript is executed, nothing can happen on your page. Most browsers have only one thread for the UI and the JavaScript and because of this they can only do one thing at a time. The longer the JavaScript takes to load and parse, the longer it takes the browser to be able to do something with user input. The very existence of a script tag in your page is enough to make the page wait.

In this article I will outline some techniques to handle this issue in a pragmatic way. The blocking issue is important because speed and responsiveness is vital for a website. The faster a user perceives your website, the sweeter the experience is to them. The Yahoo! developer blog has a lot of data on this.

A reason why

There is a reason why browsers block page loading when they encounter a script tag. Blocking is a necessary part of the page life cycle because the script may make changes to the page while executing. An example is document.write which is often used by banners. The browser can never know what that document.write does with the current DOM. So it stops rendering and lets the script execute. Once that is done, the browser resumes building your page. This also goes for external scripts. Keep in mind that page rendering and user interaction can not happen during the execution of javascript.

Position

Back in the day when separation of structure, logic and styling became a best practise, it was advised to put all the external stuff in the head of the page. This would be an ideal example of that:

<html>
<head>
	<title>2003</title>
	<script type="text/javascript" src="prototype.js"></script>	
	<script type="text/javascript" src="effects.js"></script>
	<script type="text/javascript" src="common.js"></script>

	<link type="text/css" rel="stylesheet" href="style.css" />
</head>
<body>
	<h1>Meh.</h1>
</body>
</html>

In this 'once' excellent code we can see a major performance issue. Because there are three script tags in the header the browser has to parse them first before she starts to render the rest of the page. Let's say each file is big and takes about 100ms to download and parse. The perceived performance will suffer horribly because you have to wait about 300ms of blank page before you see something. A nice example is http://www.schiphol.nl. They have a stunning 28 JavaScript files loaded in the head of their homepage. Try to load this website with an empty cache and see the nasty stuff happen. For fun try the same from Italy. The experience is very slow and I would not like to come back to check the departure times again.

schiphol

A simple solution for this would be to put the script tags just above the body end tag. Now the browser will render all the sweet stuff first and then blocks near the end to download and parse the JavaScript. If it blocks the UI in the end, it's a lot less harmful.

Combining

Now that your script tags are placed at the bottom, you still have multiple HTTP requests. An HTTP request generates overhead and is therefore harmful for the responsiveness en perceived speed of your website. A general rule of thumb that we have learned from Yahoo! is to combine your scripts. Three requests of 20kb take much longer than one request of 60kb.

Combining can be an automated process, so you don't have to bother putting all those files together yourself. The cool thing is that because of this automation you can still have separate files when developing. When the website is on a production server all JavaScript files get combined so there are as little requests as possible.

There is another great benefit from automating the combination of files. The combined file can also be compressed! This way you loose white space, comments and variable names get shortened. The smaller your file, the faster the download, the sweeter the experience becomes for you users.

Nonblocking

Nowadays it's a best practise to practise the techniques mentioned above. Next to this it is advisable to keep the amount of code small. Less code means a faster site because there is not much to download and parse. But, if we look at this best practise with realistic eyes, it's not going to work out. Our applications are bigger and fancier then ever before.

If you combine all files and put that file at the bottom, you still end up with one big file that holds off the document.ready event when downloading and parsing. The trick is to load the JavaScript after the page is finished loading. So after the window.load event.

Defer

There are a couple of techniques you can use to accomplish this. There is the defer attribute that you can place in your script tag. The fact that you pace the defer attribute on the script tag means that you tell the browser that the script will not change the DOM and it is safe to load the script after the page has been rendered. The downfall to this technique is that it is only supported by Internet Explorer and Firefox 3.5+.

Dynamic script elements

The DOM allows you to dynamically add and remove elements to it. Things like a link tag and a script tag are also DOM elements. You can change, add or remove them in any way you like with some basic DOM manipulation functions profited by JavaScript.

var scriptTag = document.createElement("script");
scriptTag.src = "file.js";
document.getElementsByTagName("head")[0].appendChild(scriptTag);

The newly generated script tag wil load file.js. The fancy thing about this technique is that the JavaScript file is loaded and executed without blocking the page while loading. It does not even matter where the script tag is placed.

When a file is downloaded this way it is automatically executed. This could potentially lead to some trouble with script dependencies. You have to find a way to know when the script is loaded, and ready to be executed. Most browsers fire a "load" event when the script source has been downloaded. You can add this to your function:

scriptTag.onLoad = function() {
	alert("I am loaded");
}

Of course Internet Explorer has a different implementation. They have implemented 5 ready states. The most important are "loaded" and "complete". Internet Explorer uses them inconsistently so it is wise to check for both ready states.

scriptTag.onreadystatechange = function() {
	if(scriptTag.onreadystatechange == "loaded" 
	|| scriptTag.onreadystatechange == "complete") {
		scriptTag.onreadystatechange = null;
		alert("I am loaded");
	}
}

If you combine everything you could write something like this:

function loadScript(url, callback) {
	var scriptTag = document.createElement("script");

	// check for IE
	if(scriptTag.readyState) {
		scriptTag.onreadystatechange = function() {
			if(scriptTag.readyState == "loaded" 
			|| scriptTag.readyState == "complete") {
				scriptTag.onreadystatechange = null;
				callback();
			}
		}	
	} else {
		scriptTag.onLoad = function() {
			callback();
		}
	}
	
	scriptTag.src = url;
	document.getElementsByTagName("head")[0].appendChild(scriptTag);	
}

With this fancy piece of code you can load script asynchronously across browsers. And with the callback attribute you can make sure that your scripts are loaded in the correct order:

loadScript("jquery.js", function() {
	loadScript("validate.js", function() {
		loadScript("common.js", function() {
			yourApplicationNameSpace.init();
		});
	});
});

For the most optimal speed, just inline this function just before the body ends. For the fastest possible load time, use a tool like YUI compressor to make the script as small as possible.

That wraps it up, I hope it helps! Let's make sure we all use these kind of techniques so we can enjoy a faster web!

Comments

  • avatar

    Floris Benniks

    Posted 1 year, 2 months ago.

    Kan je een demo online zetten? html + wat js ?:)

  • avatar

    Tim Benniks

    Posted 1 year, 2 months ago.

    A small demo!
    http://non-blocking-js.timbenniks.nl

  • avatar

    Ezra Pool

    Posted 1 year, 2 months ago.

    Als het goed is blocken de nieuwste browsers niet meer op javascript. Dit is ook de reden dat document.write niet meer is toegestaan in XHTML.

    Ook is je scriptje om javascript te laden erg leuk, maar de meeste javascript frameworks hebben betere methodes om je javascript pas na het bouwen van de DOM te laden.

    Ook kan ik de Google Closure compiler over die van YUI aanraden, de code is net wat kleiner dan. Ook gzip is zeker geen overbodige luxe.

    Over het combineren van de scripts ben ik niet helemaal eens, maar het ligt eraan hoe de code in elkaar steekt. Ik combineer bijvoorbeeld nooit jQuery plugins met de jQuery core omdat ik niet alle plugins op alle pagina's gebruik en deze dan niet geladen hoeven te worden. Als ze later nodig zijn zit de jQuery core en andere plugins al in je cache en hoeft alleen de code van de plugin gedownload te worden.

    En als laatste mis ik het gebruik van een CDN als google of amazon om je code te laden. Dit kan het laden van een framework zeer versnellen aangezien veel websites van dezelfde frameworks gebruik maken en deze dan al in je cache aanwezig zullen zijn. Ook zijn veel CDN networks net even iets sneller dan je eigen server en zijn globaal verspreidt voor betere latency.

    Verder een erg informatieve post!

  • avatar

    Tim Benniks

    Posted 1 year, 2 months ago.

    Hi Ezra,

    Thanks voor je comment.

    Nieuwe browsers blocken alleen niet meer als je het async of defer attribuut meegeeft aan de script tag. Daarmee vertel je ze als het ware dat je zeker weet dat je script de DOM niet aanpast. Heel cool dat ze hiermee komen, het is alleen niet corss-browser bruikbaar.

    Dat scriptje is inderdaad geinig, maar het is voornamelijk bedoelt als voorbeeld. In real-life projecten zou ik iets als LAB.js of de YUI3 loader gebruiken. Die zijn een stuk sexier. Als ik die nu als voorbeeld had gegeven dan blijft dit een blackbox verhaal.

    Ik moet eerlijk zeggen dat ik net pas tegen de Google Closure compiler ben aangelopen. Ik dacht dat het alleen maar een omzetter van Java naar javascript was. Thanks voor de tip. GZIP is (hopelijk) gesneden koek voor iedereen en het staat ook een beetje buiten dit onderwerp.

    Ik heb uit ervaring geleerd dat minder http requests toch sneller is dan hier en daar een extra script loaden voor een specifieke pagina. Tegenwoordig heb je veel vaker een eerste bezoeker die niet via de homepage binnen komt maar via een link op LinkedIn of Twitter. Wat mij betreft is dat de belangrijkste bezoeker en wil ik dat die maar een http call hoeft te doen voor de javascript.

    Wat de CDN betreft ben ik het met je eens. Ik denk alleen dat veel meer websites dit zouden moeten toepassen om het ook echt in de cache van users terecht et laten komen. Bijna elke website gebruikt weer een andere versie van zo'n CDN bestand en dus zijn ze elke keer anders. Wat mij betreft is dat voordeel dan helaas ook verloren gegaan.

  • avatar

    Jorrit Salverda

    Posted 1 year, 2 months ago.

    Als je kijkt naar de statistieken van CDN gebruik van bijvoorbeeld jQuery (http://trends.builtwith.com/cdn/JQuery-CDN) dan zie je dat het aantal sites dat er gebruik van maakt bedroevend laag is. De kans dat je bezoeker het al in de browser-cache heeft staan is dan ook te verwaarlozen.

    Het is in mijn ogen dan ook verstandiger om het gebruikte javascript framework en andere 3rd party libs in je ene javascript bestand van je pagina op te nemen. Wederom gaat dit om totale optimalisatie voor first time visitors.

  • avatar

    Kevin de Harde

    Posted 1 year, 2 months ago.

    Nice article. I agree with most of it, especially the part on reducing HTTP requests.

    Another side effect of non-blocking javascript has a lot of impact on progressive enhancement. You're more vulnerable to FOUC (from a DOM-changing, style-altering perspective).

    And for some browsers there will always be a need to load something in the head. HTML5 in IE anyone? You'll need something to make that work.

    I you want to load javascript on demand libraries like head.js (http://headjs.com/) have some nice tricks up their sleeves.

    And by the way, be careful when using Google Closure. It's a compiler, not just a compressor and doesn't always do the best of jobs and debugging the compiled code because of some odd behavior due to a choice the compiler made is hard. Remember: don't worry too much about file size: worry about the number of HTTP requests.

  • avatar

    Hay Kranen

    Posted 1 year, 2 months ago.

    I wrote a little dynamic Javascript loader that does basically the same thing as the loadScript() above but can also be used for loading multiple files.

    https://github.com/hay/jsdynaload

    If you want even more fancy stuff you can also try a more advanced loader like LABjs or RequireJS.

  • avatar

    Tim Benniks

    Posted 1 year, 2 months ago.

    @Kevin, I always try to be pragmatic when it comes to these kind of techniques.

    This article is very black and white and it does not tell the whole story. For example on one of my bigger projects we decided to not use this technique because it delayed the jQuery bindings on the keyboard. But we did you it for the banners. And we also added modernizr.js to the head of the page. This would be a performance penalty if you read this article without that pragmatic context.

    I will check out headjs and read a bit more about Google Closure. Thanks for the tips!