JavaScript Event Loop... to the rescue
Let’s explore how to leverage the JavaScript Event Loop to make the UI buttery smooth and responsive without blocking any of the user actions and still being able to perform extensive computations, in a theoretically single threaded JavaScript universe.
Well, I know many of you will shout back on me and say we have Web Workers
, but for the simplicity of the article and to stay in line with the underlying idea, I’d refer to it as single threaded and refrain from using Web Workers. I’m aware that the same problem statements described in the article can be solved using web workers, but the approach I’m going to discuss is easier to reason about and gels well with your existing code base without much changes.
JavaScript is single threaded and all operations are executed one after another, including the user generated actions.
We all know that JavaScript is single threaded and all operations are executed one after another, including the user generated actions (eg click
, scroll
etc) inside browsers. Now you might be wondering this would mean an utterly unresponsive UI if the underlying operations are expensive. And most of you at some point in your life, have already seen the browser alert for unresponsive script.
Here’s a demo of the problem statement.
If you run this code and click on the Show List button, the UI will get frozen and unresponsive until the showProductList
method and all its internal method calls and operations has completed. The users cannot interact with your page in the meantime. But why does this happen?
To understand this, we need to know how the operations are executed in the JavaScript runtime engines implemented inside of the browsers. This article explains in relatively simple way the Concurrency model and Event Loop
in JavaScript. You can read about it in more detail but the crux of it is that there exist a never ending, always waiting event loop that processes each of your instructions (and those generated by the user interactions with browser) one at a time, picking from a dynamic queue built out of all pending operations. The tricky fact however is that it’s not a granular operation that is processed at a time, but a stack of related operations (in our example, the whole showProductList
and the entire related call chain).
Now comes the interesting part, we can manipulate our execution by pushing parts of our operations on this queue (also known as deferred
execution) and allowing other user interactions to be queued and processed in the meantime. Result is a non-blocking, highly interactive UI. But wait, does this mean any complicated Web API to deal with low level message processing in the browsers. Well, no! Welcome the familiar setTimeout
.
Calling setTimeout
will add a message to the queue after the time passed as a second argument. If there is no other message in the queue, the message is processed right away; however, if there are messages, the setTimeout
message will have to wait for other messages to be processed. The notion of message
here means a set of instructions that are required to be processed at a time. Having the idea of setTimeout
to defer
execution, let’s explore how we can improve our user interaction and the related expensive computation.
setTimeout
along withPromise
is a powerful tool for making a highly responsive UI even with underlying large computations
First let’s try to break down the operations into chunks that can be processed independently. We would use Promise
in combination of setTimeout
to break each of the functions in the call chain into smaller operations.
We see how we converted generateId
into a deferred function that is using a combination of Promise
and setTimeout
to totally defer the execution and still giving you the ability to perform controlled operations. Now let’s make a generic defer
function that will take a function and apply the same pattern to it.
Having this generic defer
function implementation, let’s try to break our application logic into smaller units.
Processing large set of data in smaller chunks and pushing them over the event loop, would give and immediate boost in the responsiveness of the UI
getRandomProducts
can be made into a deferred function as deferredGenerateId
. However, we would want to further break the computation into smaller computations and compose the result. Let’s see how we do this.
Now let's have a look at how we break down the getList
function to process data in chunks.
And finally the showProductList
function.
Here’s the final version, with a non-blocking UI.
We see that setTimeout
along with Promise
is a powerful tool for making a highly responsive UI even with underlying large computations. We can apply this pattern quickly without much disruption to the existing code to gain some responsiveness in the code. Of course, it’s not a holy grail and for other extensively complex scenarios, one should consider investing effort in implementing solutions using Web Workers
.