JavaScript main thread. Dissected.

A look inside browsers' internals to discover how the JavaScript main thread handles our (good?) code, and how to improve and optimize it.

Ready? Let’s dig into it!


This post is not about the NBJF (a.k.a. the Next-Big-JavaScript-Frameworkemoji ™), it is instead an overview of how the JS engine is integrated within the browser (these information are mainly related to Chrome and the V8 engine, however they can easily apply with minor differences to others browsers).

Understanding the main thread is critical to handle (and debug) in the best way concurrent, time related and async tasks (widely used in modern JavaScript frameworks and libraries).

JavaScript, as you may already know, is single threaded, hence you can’t spawn new threads as you like to spread your computation cost over multiple CPU’s core for true-parallel work.

This is nice because you don’t have to worry about the concurrency issues and is still well performing for simple pages and common tasks, however, if you need to do complex and CPU-heavy stuff like images transformations or hard math client side, having multiple threads would be great in order to not block the UI (User Interface) while computing.

The UI updates and interactions are handled in the same main thread, thus if you have a long running task, for example an image transformation which needs 10s to be computed, you are blocking the UI responses (ex. JS animations, clicks, inputs, typing, etc.) for 10s, and the browser itself could decide to take action and show a popup to the user asking whether to kill that process or keep it running.

Unresponsive web page picture
This ain’t good UX…

Creating non-blocking UIs

There are however some workarounds to this problem, let’s see what you can do for running a long script as well as not blocking your UI:

  • Create a WebWorker: it is an API for running JavaScript code in a different browser’s thread. Simple and clear right? Not really, in fact this is limited to have no access to the DOM (your HTML), it can communicate with the main thread only via messages (more on WebWorkers here). It is useful for running your computation outside the main thread and once finished getting the response for it. Though if your code needs frequent access to read and manipulate the DOM, maybe this is not the best option.
  • Slice your long-task in little sub-tasks and run them asynchronously, this way between a sub-task and the next one the main thread has the opportunity to do other things (like updating the UI). You can use the setTimeout API for this and take advantage of the Event queue logic to have other things (Job queue and Rendering) done before the next Event Loop tick. (you can find your
    … ok, just joking emoji 😄)

Here I created this graphic, because visualized is better! This is the big picture of what’s happening under the hood:

JavaScript main thread architecture picture
JavaScript main thread architecture

When your code is executed it may call the Browser’s APIs to interact with the DOM or schedule some async task. Those async tasks are added to the Event queue or to the prioritizedJob queue (if using Promises). As soon as the the Call Stack has finished to process the current tick (is empty), the Event Loop feeds it with a new Tick (which is composed by ONE callback, the FULL job queue, and the POSSIBILITY to call, fully or only some parts, the Render queue).

emoji 📖 Let’s see everything one by one

  • Call Stack: it is the place where your code is executed (your functions are loaded and executed, V8 engine in Chrome and NodeJS), it is basically a LIFO stack (last-in-first-out), when it is empty, a.k.a. has completed all the current Tick tasks, it becomes ready to accept the next Tick from the Event Loop;
  • Browser APIs: a link between your code and the browser’s internals to schedule tasks, interact with the DOM and more (ex. setTimeout, AJAX, createElement, querySelector, append, click, etc.). In case of callbacks they will add your callback code to the Event queue, instead, in case of a then (promise’s method), your then-code will be added to the Job queue;
  • Event queue: every time you add a callback (ex. via the setTimeout or the AJAX APIs), it is added to this queue;
  • Job queue: this queue is reserved for promise’s thens, it is a prioritized queue, its meaning is like ‘execute this code later (= asynchronously), but as soon as possible! (= before the next Event Loop tick)’, and this is why browsers had introduced this new queue to fulfil the Promises specifications;
  • Render Queue: this will be explained soon in another article, stay tunedemoji 🎉 here the article emoji 🎉;
  • Next Tick: it is what will be executed next, basically it’s composed by ONE callback from the Event queue, THE FULL Job queue (this point is important, the current tick will finish only after the Job queue is empty, so you may inadvertently block it from going to the next Tick if you continuously add new jobs to this queue), may re-render (execute the necessary steps in the Render queue to update the screen);
  • Event Loop: it monitors the Call Stack, as soon as it is empty (has finished to process the current tick), the Event Loop feeds it with the next Tick.

emoji ⚙ Other threads

Along the main thread there are many other threads spawned by the browser to do useful stuff:

  • Parser Thread: parses your code in machine-understandable trees;
  • Statistics collector Thread: collects data and statistics to discover insights about your code (the scope is to optimize it runtime);
  • Optimizer Thread: uses the statistics and insights collected by the Statistics collector Thread to make performance optimizations over your code (Caching, Inlining, etc.);
  • Garbage Collector Thread: removes unconnected (no more referenceable from the ROOT node) JavaScript objects to free up memory using a mark-and-sweep algorithm. We don’t know when this will happen and have no control over it. AFAIK the browser uses this thread to track whose objects to remove and do useful stuff, but when it needs to remove them it actually blocks the main thread and uses it. From the Firefox blog Q:”Silly question here, why must garbage collection stop UI events and js execution? Couldn’t the GC just run in a separate thread?”, R:”It can be done, but the garbage collector is looking at the same objects that the JS currently running is touching, so it must be done carefully. That said, the Firefox GC actually does do some work on a separate thread: some types of objects can be thrown away once they are known to be garbage without affecting the main thread.”;
  • Rasterizer Thread: rasterize your graphic into frames.

emoji 📚 Further readings

How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await

What is the different between job queue and event loop queue in JavaScript?

Javascript Job Queues and Promises

Smarter garbage collection for smoother browsing and less memory usage

Incremental GC in Firefox 16!

For updates, insights or suggestions, feel free to post a comment below! emoji 🙂

Essay originally published on Medium.


Latest from Web Engineering browse all

Nested Enums in TypeScript. | cover picture
Nested Enums in TypeScript.
Created August 19 2021.
How to solve the need of nested and multi-level Enums in TypeScript.
WebAssembly + Emscripten Notes. | cover picture
WebAssembly + Emscripten Notes.
Created December 25 2020, updated August 19 2021.
My notes on learning WebAssembly and its integration with Emscripten.
Docker Notes. | cover picture
Docker Notes.
Created November 19 2020, updated December 25 2020.
My notes on learning Docker.