JavaScript Internals: Under The Hood of a Browser
How your code is executed inside a browser
Intro
Javascript is weird. Some people love it, others hate it. It has a lot of unique mechanisms which are not present and have no counterparts in other popular languages. Code execution order, for instance, which might be sometimes unintuitive, definitely stands out.
Understanding the browser environment, what it is composed of and how it works will make you more confident in writing JavaScript and well-prepared for potential issues that might happen.
In this piece, I’ll try to shed some light on what is happening under the hood of the Chrome browser. We’ll take a look at:
- V8 Javascript Engine — compilation steps, heap and memory management, and call stack.
- Browser runtime — concurrency models, event loops, and blocking and non-blocking code.
JavaScript Engine
The most popular JavaScript engine is V8 which is written in C++ and used by Chromium-based browsers such as Chrome, Opera, and even Edge.
Basically, the engine is a program that translates your JavaScript into machine code and executes the result on a computer’s central processing unit (CPU).
Compilation
When your JavaScript file is loaded by the browser, V8's parser transforms it into an abstract syntax tree (AST). This tree is used by Ignition —an interpreter which produces bytecode. Bytecode is an abstraction of machine code able to be executed by compiling to non-optimized machine code. V8 executes it in the main thread while TurboFan, the optimizing compiler, makes some optimizations in another thread and produces optimized machine code.
This pipeline is called just-in-time (JIT) compilation.

Call Stack
JavaScript is a single-threaded programming language with a single call stack. It means that your code is executed synchronously. Whenever a function runs, it will run entirely before any other code runs.
When V8 invokes your JavaScript functions it has to store the runtime data somewhere. Call Stack is the place in memory which consists of stack frames. Each stack frame corresponds to a call to a function which has not yet terminated with a return. A stack frame is composed of:
- local variables
- argument parameters
- return address
If we execute a function, V8 pushes frame on top of the stack. When we return from a function, V8 pops off the frame.

As you can see in the example above, a frame is created on each function invocation and removed on each return statement (more about what is stored in the call stack you can read here).
Everything else is allocated dynamically in a large unstructured piece of memory called the Heap.
Heap
Sometimes V8 doesn’t know at compile time how much memory an object variable will need. All the memory allocation for such data happens in the heap — unstructured region of memory. Objects on the heap live on after we exit the function that allocated the memory.
V8 has a built-in Garbage Collector (GC). Garbage collection is a form of memory management. It’s like a collector which attempts to release the memory occupied by objects that are no longer being used. In other words, when a variable loses all its references GC marks this memory as “unreachable” and releases it.
You can investigate heap by making a snapshot in Chrome Dev Tools.

Each JavaScript object that was instantiated is grouped under its constructor class. Parenthesized groupings indicate native constructors that you can’t invoke directly. As you can see there are a lot of (compiled code)
and (system)
instances, but also some traditional JavaScript objects like Math
, String
, Array
and so on.
Browser Runtime
So V8 can execute JavaScript according to the standard, synchronously, using a single call stack. But there is not much we can do with it. We need to render the UI. We need to handle user interactions with the UI. Moreover, we need to handle user interactions while making network requests. But how do we achieve concurrency when all our code is synchronous? It’s possible thanks to the browser engine.
The browser engine is responsible for rendering pages with HTML and CSS. In Chrome it’s called Blink. It’s a fork of WebCore which is a layout, rendering, and Document Object Model (DOM) library. Blink is implemented in C++ and exposes Web APIs like DOM elements and events, XMLHttpRequest
, fetch
, setTimeout
, setInterval
and so on, which are accessible via JavaScript.
Let’s consider the following example with setTimeout(onTimeout, 0)
:

As we can see f1()
and f2()
functions are pushed to the stack first and then onTimeout
is executed.
The key point is that we are registering a function to run at a later time. Whether it’s a user click or a timeout. Our asynchronous callback is executed by V8 only after the corresponding event is fired.
So how does the example above works?
Concurrency
Right after the setTimeout
function is executed — the browser engine places setTimeout
’s callback function into an event table. It’s a data structure which maps registered callbacks to events, in our case onTimeout
function to timeout event.
Once the timer expires, in our case immediately as we put 0 ms as the delay, the event is fired and the onTimeout
function is put in the event queue (aka the callback queue or message queue or task queue). The event queue is a data structure which consists of callback functions (tasks) to be processed in the future.
And last but not least, the event loop, a constantly running loop, checks whether Call Stack is empty. If so the first added callback from the event queue is executed, hence moved to the call stack.
The processing of functions continues until the call stack is once again empty. Then the event loop will process the next callback in the event queue (if there is one).
const fn1 = () => console.log('fn1')
const fn2 = () => console.log('fn2')
const callback = () => console.log('timeout')fn1()
setTimeout(callback, 1000)
fn2()// output:
// fn1
// fn2
// timeout

ECMAScript 2015 introduced the concept of the job queue (aka Micro-task queue). This queue is filled with Promise resolve
and reject
functions. Callbacks in the job queue have a higher priority of execution than callbacks in the event queue. Which means that the event loop will execute all of them one by one before any other callback in the event queue.
const fn1 = () => console.log('fn1')
const fn2 = () => console.log('fn2')
const onTimeout = () => console.log('timeout')
const onResolve1 = () => console.log('resolved1')
const onResolve2 = () => console.log('resolved2')fn1()
setTimeout(onTimeout, 0)
Promise.resolve()
.then(onResolve1)
.then(onResolve2)fn2()// output:
// fn1
// fn2
// resolved1
// resolved2
// timeout

Pay attention to the execution order of onResolve1
, onResolve2
and onTimeout
callbacks.
Blocking vs Non-Blocking
In simple terms, all the JavaScript code is considered blocking. While V8 is busy with processing stack frames — the browser is stuck. The UI of your app is blocked. The user won’t be able to click, navigate, or scroll. Responses from your network requests won’t be processed until V8 finishes its work.
Imagine that you have to parse an image in your program that’s running in the browser.
const fn1 = () => console.log('fn1')
const onResolve = () => console.log('resolved')
const parseImage = () => { /* long running parsing algorithm */ }fn1()
Promise.resolve().then(onResolve) // or any other Web API async fn
parseImage()

In the example above, the event Loop is blocked. It can’t process callbacks from event/job queue because the call stack contains frames.
Web API gives us a possibility to write non-blocking code via asynchronous callbacks. When calling functions like setTimeout
or fetch
, we are delegating all the work to C++ native code which runs in a separate thread. As soon as the operation is completed the callback is put to Event Queue. Meanwhile, V8 can continue further execution of JavaScript code.
With such concurrency model, we can handle network requests, user interactions with UI and so on without blocking the JavaScript execution thread.
Outro
Understanding what the JavaScript environment is composed of is crucial for every developer who wants to be able to solve complex tasks. Now we know how asynchronous JavaScript works, the role of the call stack, event loop, event queue and job queue in its concurrency model.
As you might have guessed there is a lot more going on behind V8 engine and browser engine. However, most of us simply need to have a basic understanding of all these concepts. Please, click the clap 👏, if the article above was helpful for you.