How JavaScript Works Internally: A Deep Dive for Beginners
Before diving into the internals of JS, let's first understand how it executes code in a real-world scenario.
when we visit a webpage that runs JavaScript
- The browser loads the webpage (HTML and CSS).
- JavaScript is fetched and executed by the JavaScript engine.
- The code is parsed, complied and executed.
- If the code has any async task (e.g.,
fetch()
,setTimeout()
etc.), JavaScript doesn't block it, instead, it uses the event loop to handle them.
Basically, JavaScript is high-level, interpreted, single-threaded and asynchronous programming language. It runs one task at a time. However, through the event loop and task queues, it handles asynchronous operations without blocking the main thread.
Now that we have a high-level understanding, let's break it down further -
-
JavaScript executes using an engine (Different browsers has different engine).
-
Execution Context:
- Global Execution Context - Created when JavaScript starts. It manages the global variables and functions.
- Function Execution Context - Created every time when a function called. It maintains its own scope.
-
Hoisting moves variable and fucntion declarations to the top of thier scope before execution.
-
Call Stack tracks the function execution
-
Memory Management:
- Stack stores primitive values.
- Heap stores objects, arrays and functions
-
Event Loop and Task Queue ensure async operations.
-
Microtasks execute before Macrotasks.
-
Just-In-Time (JIT) Compilation optimizes JavaScript execution for faster performance.
Now let's deep dive in each step to understand it fully.
1. The JavaScript Engine
JavaScript Engine is the core component that interprets and executes JS code. Every JavaScript engine has two key components:
- Execution Context Memory (Call stack/ Call Frame) - Manages function execution and stores primitive values.
- Heap Memory - Stores dynamically allocated objects and functions.
🪜 Steps of JavaScript Execution -
- Parsing - JavaScript code is converted into an Abstract Syntax Tree (AST).
- Compilation - The AST is converted into machine code using Just-In-Time(JIT) compilation.
- The optimized machine code is run.
⚙️ Popular JavaScript Engines
- V8 - Used in chrome and Node.js.
- SpiderMonkey – Used in Mozilla Firefox.
- JavaScriptCore – Used in Apple’s Safari.
2. Execution Context and Hoisting
An Execution context is the environment in which JS code runs. It has two main phases:
-
Creation phase
- The Global Execution Context is created first.
- Memory is allocated for variables and functions.
- Hoisting occurs here.
-
Execution phase
- Code runs line by line.
- Variables are assigned values.
- Function calls create new Function Execution Contexts inside the Call Stack.
3. Call Stack: Function Execution Flow
Functions executes using Call Stack. It follows the Last-in, First-out (LIFO) principle.
Example -
function first() {
second();
console.log("First function executed");
}
function second() {
console.log("Second function executed");
}
first();
// Output
// Second function executed
// First function executed
first()
is pushed onto the stack.- inside
first()
,second()
is called and push onto the stack. second()
executes, and popped from the stack.first()
resumes and executes and popped from the stack.
4. Memory Management
JavaScript manages memory efficiently through:
-
Call stack
- Stores primitive values (numbers, strings, booleans etc.).
- Stores function execution contexts.
- Memory is automatically cleared after function execution.
-
Heap Memory
- Stores objects, functions, and arrays (dynamic memory allocation).
- Memory persists beyond function execution.
- Managed by Garbage Collection.
5. The Event Loop & Asynchronous JavaScript
As we already know that JavaScript is single-threaded, meaning it can execute only one task at a time. If JavaScript executed long-running tasks synchronously, it would block the entire webpage or server.
To prevent blocking, JavaScript offloads certain tasks like timers, networks requests, and I/O operations etc. to the browser's Web APIs or NodeJs APIs so they can be handled separately without freezing the main execution thread.
The Event Loop and Task Queue allows JavaScript to handle async operations while maintaing its single-thread nature.
🔄 How Event Loop Works
-
Synchronous code runs first
- The Call Stack executes Synchronous code line by line.
- Any function that completes immediately is executed and removed from the stack.
-
Asynchronous tasks are sent to the environment's background processing system
- If an async task is encountered, JavaScript does not block execution.
- Instead, the task is sent to the Web APIs in browsers or Node.js APIs in a server environment, which handle it in the background.
-
Completed Async Tasks Move to the Task Queue
- Once an asyc operation is finished, its callback function is moved to the Task Queue.
- But it does not exceute immediately. It waits for the call stack to be empty.
-
The Event loop monitors and pushes tasks
-
The Event loop continuosuly checks
- Is the call stack empty.
- Are there any pending tasks in the Task Queue.
-
If the call stack is empty, the event loop picks the next task from the queue and push it onto the call stack and execute.
-
🔥 Example
console.log("Start");
setTimeout(() => {
console.log("setTimeout callback");
}, 0);
console.log("End");
// output:
// Start
// End
// setTimeout callback
in the above example, we can see that:
console.log("start")
runs immediately and logs "Start".setTimeout()
is encountered. JavaScript sends this to the environment's background processing system (Web API or Node.js API) and timer of 0ms starts, but the execution of code continues.- Next synchronous task (
console.log("End")
) runs and logs "End". - The Call stack is now empty. The Event Loop checks the pending tasks and
setTimeout()
callback moves from the Task Queue to the Call Stack. - Now callback executed and logs "setTimeout callback".
6. Microtasks & Macrotasks
JavaScript maintains two seperate queues for async tasks:
-
Microtasks Queue (Higher Priority)
- It runs as soon as call stack is empty.
- Microtasks are Promises (Promise.then(), catch(), finally()) and MutationObserver.
-
Macrotask Queue (Lower Priority)
- It runs after all microtasks finish.
- Macrotask are setTimeout(), setInterval(), setImmediate(), I/O tasks (like file reading, network requests)
🔥Example: Microtasks & Macrotasks Execution Order
console.log("Start");
setTimeout(() => console.log("Macrotask"), 0);
Promise.resolve().then(() => console.log("Microtask"));
console.log("End");
// output:
// Start
// End
// Microtask
// Macrotask
console.log("start")
runs immediately and logs "Start".setTimeout()
is encountered. JavaScript sends this to the environment's background processing system (Web API or Node.js API) and timer of 0ms starts, but the execution of code continues.Promise.resolve().then()
is added to the Microtask Queue.- Next synchronous task (
console.log("End")
) runs and logs "End". - The Call Stack is now empty, so the Event Loop processes the Microtask Queue first and logs "Microtask".
- The Call stack is now empty. The Event Loop checks the pending tasks and
setTimeout()
callback moves from the Task Queue to the Call Stack. - Now callback executed and logs "Macrotask".