blog

JavaScript and Non-blocking functions

One of the most interesting features of JavaScript must be its event-driven and asynchronous nature: the operations can, but don't need to block the next operation from being executed before the current one is done. For instance, the following snipped follows a very logical sequence:

console.log(1);
console.log(2);
console.log(3);
console.log(4);

// The output is: 1 2 3 4

However, we can make that these functions are executed in a different sequence by setting timeouts for them:

setTimeout(() => console.log(1), 75);
setTimeout(() => console.log(2), 0);
setTimeout(() => console.log(3), 50);
setTimeout(() => console.log(4), 25);

// The output is: 2 4 3 1

Why is this useful? Well, if we have operations that are costly to perform, but don't have a high priority. Normally, they would block other operations, even though they are more important and don't need the former:

// Not very important operation
for (let i = 0; i < 1000000000; i++);
console.log('Not very important operation is done!');

// Very important operations
console.log('Super important operation');
console.log('This operation is also very important');

/* Output:
Not very important operation is done!
Super important operation
This operation is also very important
*/

In this case, we could simply move the costly and unimportant operation to the end of the file (since it is not used by anything else, after all), but real life is not that easy: although we should prioritise the interaction with the user while leaving costly and unimportant operations for last, the interactions with the user are not predictable: we cannot create a logical sequence that attends to all the cases. However, we can use the setTimeout function and set a 0 (zero) timeout for a procedure: the operation will be sent to the back of the queue of operations to perform. Like in this case:

// Not very important operation
setTimeout(() => {
    for (let i = 0; i < 1000000000; i++);
    console.log('Not very important operation is done!');
}, 0);

// Very important operations
console.log('Super important operation');
console.log('This operation is also very important');

/* Output:
Super important operation
This operation is also very important
Not very important operation is done!
*/

Having this in mind, I started experimenting on what is the best combination to create a script that does the most vital (and cheap) operations as soon as possible, but leaves the ones that would affect the user experience for last.

First, I made a simple webpage like this one (I replaced the angle brackets with square brackets because Wordpress was screwing up with my page. Just use your imagination):

page.html
<html>
<head>
<script src="1.js"></script>
</head>
<body>
<div id="overall">
  ...around 100,000 auto-generated HTML elements here...
</div>
<script src="2.js"></script>
</body>
</html>

1.js
alert('Script 1 ' + document.getElementById('overall').childNodes.length);
for (let a = 0; a < 1000000000; a++);
alert('Script 1 done');

2.js
alert('Script 2 ' + document.getElementById('overall').childNodes.length);
for (let b = 0; b < 1000000000; b++);
alert('Script 2 done');

(I will change the file 1.js during this post, but 2.js and page.html will stay the same)

The idea is simple: a very heavy webpage with a script in the header, and a script at the end of the DOM; these scripts are just alerts saying how many elements are there in the DOM. This was the order of what happened while loading the page:

1- Script 1, 0 (alert in a blank page) 2- A few seconds of a blank page 3- Script 1 done 4- Dom is loaded 5- Script 2, 100002 (alert in a fully-loaded page) 6- A few seconds of loading, but with the page fully functional 7- Script 2 done

* the first childNode.length actually gives an error because childNodes wasn't even defined yet, but the moral is: the DOM is not loaded

This is why it is recommended to put your script at the end of the page: it will not block your DOM from rendering. On top of that, if you are planning to do some DOM manipulation, you have to wait for it anyway, otherwise there won't be anything to manipulate (duh).

However, it has a drawback: your script will only be called after the DOM is already rendered. For our case, we want to know how much time it took for the DOM to load, so this is not an acceptable alternative. What we can do in this case is use an event to see when the page gets loaded, and then we execute the script:

1.js

alert('Doing some very fast and important work here...');
document.addEventListener("DOMContentLoaded", function () { 
  alert('Script 1 ' + document.getElementById('overall').childNodes.length); 
  for (let a = 0; a < 1000000000; a++); 
  alert('Script 1 done'); 
});

With this, the orders of executions become:

1- Doing some very fast and important work here 2- Dom is loaded 3- Script 2, 100002 (alert in a fully-loaded page) 4- A few seconds of loading, but with the page fully functional 5- Script 2 is done 6- Script 1, 100002 (alert in a fully-loaded page) 7- A few seconds of loading, but with the page fully functional 8- Script 1 done

Now another problem arrives: what if there are several costly, but less important functions inside that one? Say this is our 1.js now:

1.js

alert('Doing some very fast and important work here...');
document.addEventListener("DOMContentLoaded", function () {
  alert('Doing not very important operation...');
  for (let a = 0; a < 1000000000; a++);
  alert('Not very important operation done');

  alert('Super important operation');
  alert('This operation is also very important');
});

The order of operations would be:

1- Doing some very fast and important work here 2- Dom is loaded 3- Script 2, 100002 (alert in a fully-loaded page) 4- A few seconds of loading, but with the page fully functional 5- Script 2 done 6- Doing not very important operation... 7- Not very important operation done 8- Super important operation 9- This operation is also very important

Can we send the "Not very important operation" to the back of the queue again? Yes we can. By using the setTimeout function I described before:

1.js
alert('Doing some very fast and important work here...');
document.addEventListener("DOMContentLoaded", function () {
  setTimeout(() => {
    alert('Doing not very important operation...');
    for (let a = 0; a < 1000000000; a++);
    alert('Not very important operation done');
  }, 0);

  alert('Super important operation');
  alert('This operation is also very important');
});

This is the order of operations we would get:

1- Doing some very fast and important work here 2- Dom is loaded 3- Script 2, 100002 (alert in a fully-loaded page) 4- A few seconds of loading, but with the page fully functional 5- Script 2 done 6- Super important operation 7- This operation is also very important 8- Doing not very important operation... 9- Not very important operation done

By using some timeouts and some events, I'm confident we will be able to make a client module that will execute at the right time: without interfering in the user experience, but still doing the right operations in the right time.