Browser Event Loop – How to Use Asynchronicity to Unfreeze the Browser

Ever had to perform a task in the browser that’s so large it freezes it? There’s a way around it, so join me in learning how to keep the UI responsive and make the user experience pleasant even when there’s some heavy lifting happening in the back…

Before we dive into the specifics of setTimeout and asynchronicity in JavaScript, it is necessary to first understand the browser Event loop. I’ll try to give you a high level overview of the model.


Call stack

Let’s start with the call stack. When invoking a function, a stack frame is created and put on the top of the stack. The stack frame contains function arguments and its local variables.

When another function is invoked from inside the first one, another stack frame is created and put on top of the invoking function, and so on. When the function on top returns, its stack frame is taken(popped) out of the stack. When every function returns the stack is empty and another function can be processed.

Event table

Event table is a place where event handling functions are put. Let’s consider the following example:

document.addEventListener('click', function () {
    console.log('you\'ve touched the document');
});

When this line is executed, the call stack tells the event table to register that particular function as a callback for the event we specified (‘click’ in this case) and to move it to the event queue when that event occurs.

So the event table is actually a book where the browser searches for callbacks for particular events. It moves all the callbacks registered for that event (when the event happens) to the event queue to execute them at first moment available, or it does nothing if there are no callbacks for that event.

Event queue

Finally a message queue is a place where all callbacks that need to be executed land. When you click on an element the browser looks up the event table if there are any callbacks registered for that event and if so, push them on the queue in the order in which they were registered.

Event loop

The event loop checks if the call stack is empty and if so, it looks if there are any functions on the queue waiting to be invoked. If there are, it takes the first one, creates a stack frame on the call stack and invokes the function within that stack frame. When the function returns and the stack is empty again, the event loop repeats the process from the beginning.

Note that while the stack is occupied (means code is being executed) no other code can be processed at the same time

event loop diagram

Event loop diagram

Asynchronicity

Now that we know all that, we can consider the setTimeout function which is used to make async calls.

setTimeout takes a function and a number (indicating milliseconds) as parameters:

setTimeout(function () {
    /* do something here */
}, time);

What setTimeout really does is pushing the given function to the event queue (not executing!) after a specified time has passed. Let’s say we want to log a message into the console, after some time. We write:

setTimeout(function () {
    console.log('Hello World');
}, 2000});

What this statement does is writing to the event table: add this function to the event queue after two seconds have passed from now. It does NOT mean “Execute the given function after two seconds”!

The browser checks the event table and if the time comes it adds the callback we gave to setTimeout to the event queue. If there are no other messages on the queue and the stack is empty then our function will be called immediately, but if the stack is occupied or there are other callbacks waiting to be executed in front of our setTimeout callback then our function will have to wait.

Freezing the Browser

Okay, we know now that async call means adding a function to the event queue. But how does this fact help us be more responsive?

Lets write some code which does put a lot of work on the browser. In this example I will add 150,000 div elements to the body element of the document. This guarantees that the script will run at least for couple of seconds freezing it for that period of time. I will then modify the script using setTimeout to show you how to make the browser responsive while still doing that heavy job.

// total number of div elements we will add to body
var divNum = 150000;

// number of divs  we will add in one generateDivs invocation
var partSize = 1000;

// iteration counter
var iteration = 0;

// the body element to which we append divs
var body = document.getElementsByTagName('body')[0];

function generateDivs () {
    var newDiv, j;

    iteration ++;

    for (j = 0; j < partSize; j++) {
        newDiv = document.createElement('div');
        newDiv.textContent = 'div nr: ' + (j + iteration * partSize);
        body.appendChild(newDiv);
    }

    if (iteration < divNum/partSize) {
        generateDivs();
    }
}

genarateDivs();

What this script does is:

  • add 1000 divs to body elements,
  • increase the iteration counter,
  • check if we added all the elements we wanted to, if no execute generateDivs again.

There is also a button that changes background color on click. This button is to demonstrate that the script executing the event handler – change the background color – has to wait on the queue until the stack is empty and it can be invoked. The page is unresponsive for the duration of the code being executed on the stack.

So what happened? When you run the above code your browser will become unresponsive for a few seconds (4 in my case)  and then all the added elements will appear. While the script is executing the stack is occupied and event callbacks are waiting on the queue for their turn. This means  that when you click a button, its event callback is added to the queue but it can’t be executed because the stack is occupied. After all the div elements are added the stack is free and the event loop can get next element from the queue and call it on the stack. That’s why the button is responsive again only after the script has finished.

Example fiddle.

Obtaining Responsiveness

Now I want to show you how we can turn this script into a nonblocking one, and division of the problem is pivotal to accomplish this.

So as you can see sync calls (the recursive generateDivs function calls) are adding next stack frame to the call stack, while async calls are adding to the event queue. Writing nonblocking code is about adding to event queue.

Let’s now modify our code so it adds to the queue instead of increasing the stack size.

// total number of div elements we will add to body
var divNum = 150000;

// number of divs  we will add in one generateDivs invocation
var partSize = 1000;

// iteration counter
var iteration = 0; 

// the body element to which we append divs
var body = document.getElementsByTagName('body')[0];

function generateDivs () {
    var newDiv, j;

    iteration ++;

    for (j = 0; j < partSize; j++) {
        newDiv = document.createElement('div');
        newDiv.textContent = 'div nr: ' + (j + iteration * partSize);
        body.appendChild(newDiv);
    }

    if (iteration < divNum/partSize) {
        setTimeout(generateDivs, 0); // < – generateDivs()
    }
}

genarateDivs();

The whole modification done here is to pass generateDivs as a callback to setTimeout instead of just invoking it.

Now because we are adding to the queue instead of the stack we make the browser unresponsive only for small amounts of time. Time needed to add 1000 divs instead of 150,000. This way when we click on the button the event handler function can get in between subsequent generateDivs function invocations and get invoked almost instantly, that way we turned the unresponsive into responsive, by division of a large task into small ones, taking little time, and calling each of these tasks asynchronously.

Example fiddle.