How Javascript works? Tough question which can be approached by describing the core elements of Javascript engines. The event loop, the render steps and the WebAPIs.
As Javascript developers, we all know the importance of performance. Now more than ever the browser has a lot of responsibilities among data fetching, calculation, ui inputs, animations, you name it. To tackle all these tasks, Javascript has only one thread available.
Javascript is Single-threaded. All tasks Javascript runs are sequentially processed by the thread. We can sum it up like this:
Where other languages use multi-thread to run instructions concurrently. Javascript can only run one instruction at a time. This means, if a task takes too much time, the application is frozen. To sum it up:
one thread === one task at a time.
The question that comes in mind is "How can the browser manage all the tasks without freezing?
The event loop
The event loop is the way Javascript Engines orchestrate tasks in order to be processed. The concept is simple. There is a handless loop where Javascript wait for tasks and execute them.
Examples of tasks:
-
When an external script
<script src="...">
loads, the task is to execute it. - When a user moves their mouse, the task is to dispatch a mousemove event and execute handlers.
- When the time is due for a scheduled setTimeout, the task is to run its callback.
Let’s put it into motion with a simple bit of code. We assume we have an HTML page which include this script.
console.log("Before");
function handleTimeout() {
console.log("setTimeout script");
}
setTimeout(() => handleTimeout, 0);
console.log("After");
-
console.log("Before")
print "Before". -
setTimeout
wait 0 milliseconds and queue a task which invoke the callback function. -
console.log("After")
print "After". -
console.log("setTimeout script")
print "setTimeout script".
As we can see the event loop process the script which is a task. During
that processing, the script creates a new task with setTimeout. The new
task represents the execution of the handler so
console.log("setTimeout script")
is
executed.
What about the render steps?
One type of the tasks I did not mention yet and which is often ignored by front developer is render steps. When you change the DOM, the browser has to update what's on the screen. This is done through render steps. Render steps are basically a set of tasks. Here's a quick recap :
- Style calculation where CSSOM and DOM trees are combined into a render tree.
- Compute the layout of each visible element.
- The paint process that renders the pixels to screen.
As you now guess, these tasks also have to compete to be processed by the event loop. The figure below show four tasks mutating the DOM therefore triggering the render steps.
Each task has to wait before the three render steps finishes and conversely. All tasks being dependent on the execution time, if one of our script takes too much time (meaning more than few millisecond) the application will freeze.
In fact, the event loop is smarter.
In javascript a frame represents the three steps the browser does in one event loop iteration to update the screen.
Scientists and researchers agree that the human eye can see until 60 frames per second. 60 Hz refresh rate is what the browser is aiming for a good responsive user experience. This means the browser should process a frame in about 16.7ms. During this 16.7ms the event loop prioritizes other Javascript tasks. Therefore, many tasks can be executed before the render steps occur.
Now we know that the event loop is smart, but What about fetching data from the server? This take more than a few milliseconds, even a few seconds in the worst case. This is where Web APIs come in.
Web APIs and asynchronous code
We all use Fetch(), console.log(), setTimeout()... without caring too much how it works. This is because Web APIs are not part of the Javascript language itself. Web APIs are built into your web browser. This is important, because it means they do not have the same restrictions, thus they can use multi threads. WebAPIs runs in parallel with the event loop and insert a new task when their job is done. This is how asynchronicity works in Javascript.
Let's illustrate this with a simple example using fetch API.
fetch('http://example.com/data.txt')
.then(res => res.text())
.then(text => show(text));
Here it what's happening in the event loop
- First the fetch API calls the URL. This call happens outside of the event loop scope.
-
Then the first callback
response.text()
is sent to the event loop as a task. -
res.text()
is also part of the fetch API, so another task is generated which representsdisplayText(text)
. show(text)
is executed.- The browser print the text through the render steps.
Non blocking instructions happens thanks to Web APIs. Whether you pass your callback as a function parameter or via promise, the result is the same. Callbacks are the glue between our script and the Web APIs.