DEV Community

Naga Rohith
Naga Rohith

Posted on

The Language Behind the Web: How JavaScript Works!

JavaScript is the language behind the web, powering almost everything you see and interact with online. But beyond the code we write, there’s a complex system at work, parsing, compiling, and executing every instruction with precision.

In this blog, we’ll explore how JavaScript truly works under the hood: how the engine runs your code, manages execution, handles asynchronous tasks, and cleans up memory. Understanding these internals will help you write smarter, more efficient code and see JavaScript from a whole new perspective.

Javascript Engine Overview

At its core, JavaScript is a single-threaded, interpreted (or just-in-time compiled) language that powers interactivity on the web.

Being single-threaded, JavaScript executes one task at a time in a single main thread. Yet, it can handle network requests, animations, and user interactions without freezing the page. This is possible because, while JavaScript itself is synchronous, it achieves asynchronous behavior through callbacks, promises, and the event loop.

Every time you run JavaScript, an engine called V8 in Chrome and Node.js, SpiderMonkey in Firefox, or JavaScriptCore in Safari, is at work. The engine parses, compiles, and executes your code in three main stages.

  1. Parsing: The code is read and transformed into an Abstract Syntax Tree (AST), a structured representation of the program.
  2. Compilation: Modern engines use Just-In-Time (JIT) compilation, combining interpretation with on-the-fly optimization for better performance.
  3. Execution: The code runs inside an Execution Context, which is managed by the Call Stack.

This hybrid process ensures your code runs efficiently while enabling the synchronous engine to handle asynchronous tasks seamlessly.

Execution Context

When a JavaScript program runs, the engine first creates the Global Execution Context (GEC), the base environment where all code starts executing. There is only one GEC per program. It has two main phases:

-> Creation Phase

  • Allocates memory for variables and function declarations.
  • Defines the Global Object (window in browsers, global in Node.js).
  • Sets up this to reference the Global Object in the global scope.
  • Performs hoisting, which allocates space for variables and functions before execution begins.

-> Execution Phase

  • Executes code line by line.
  • Assigns values to variables.
  • Invokes functions, each of which creates its own Function Execution Context (FEC).

The Function Execution Context behaves similarly to the GEC but with a few key differences:

  • Each function call creates a new FEC.
  • Contains its own Variable Environment for local variables and parameters.
  • Has its own this value, determined by how the function is called.
  • Executes in two phases, creation and execution, just like the GEC.

Execution contexts are managed using the Call Stack, ensuring that the engine executes functions in the correct order. Once a function finishes, its FEC is removed from the stack, and control returns to the previous context.

In short: The Global Execution Context is the base environment for your program, while each Function Execution Context handles the execution of individual functions. Together, they provide the framework for how JavaScript organizes memory and executes code.

The Call Stack

Think of the Call Stack as your program’s to-do list. It keeps track of which functions are currently running and what needs to run next. Whenever a function is called, a new Function Execution Context (FEC) is created and pushed onto the stack. Once that function completes, its context is popped off, allowing JavaScript to return to the previous task in order.

Let's illustrate:

function one() {
  console.log('First');
  two();
}

function two() {
  console.log('Second');
}

one();
console.log('Third');
Enter fullscreen mode Exit fullscreen mode

Here's what happens:

  1. GEC is created and pushed to the stack.
  2. one() is called → FEC for one() is created.
  3. Inside one(), two() is called → FEC for two() is added.
  4. two() finishes → popped off.
  5. one() finishes → popped off.
  6. Back to global → prints Third.

In short: The Call Stack manages the order of function execution. JavaScript runs one function at a time, always finishing the most recent one before returning to the previous, just like crossing items off a to-do list from the top down.

Hoisting and the Temporal Dead Zone (TDZ)

Hoisting is JavaScript’s process of allocating memory for variables and functions during the creation phase of the execution context, before any code runs. This gives the illusion that declarations are “moved” to the top of their scope (though only in memory, not physically in the code).

The Temporal Dead Zone (TDZ) is the period between hoisting and the actual initialization of let and const variables. During this phase, the variables exist in memory but are uninitialized and cannot be accessed. Any attempt to use them before their declaration line results in a ReferenceError, ensuring safer and more predictable code behavior.

how the hoisting and TDZ works:

-> Function Declarations:

  • Fully hoisted and initialized before execution starts.
  • Can be invoked anywhere in their scope, even before their definition in code.

-> Variable Declarations:

  • var variables are hoisted and initialized with undefined.
  • let and const are hoisted but remain uninitialized until their declaration line, the time between hoisting and initialization is called the Temporal Dead Zone (TDZ).
  • Accessing them in the TDZ results in a ReferenceError.

-> Function Expressions and Arrow Functions:

  • When defined using var, they behave like variables, hoisted and initialized as undefined, causing errors if called before declaration.
  • When defined with let or const, they’re also subject to the TDZ and cannot be accessed before initialization.
greet(); // Works — function declaration is hoisted
sayHello(); // TypeError — sayHello is undefined
console.log(num); // ReferenceError (TDZ)

function greet() {
  console.log("Hello!");
}

var sayHello = function () {
  console.log("Hi!");
};

let num = 10;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • During the creation phase, the engine fully hoists greet() and stores it in memory.
  • sayHello, declared with var, is hoisted but set to undefined, calling it before initialization throws a TypeError.
  • num, declared with let, is hoisted but remains in the TDZ until its line of execution.

Key takeaways:

  • Function declarations take priority in hoisting.
  • Variables declared with var are initialized with undefined.
  • let, const, and function expressions are hoisted but uninitialized (TDZ applies).
  • Understanding these differences helps prevent subtle reference and type errors in your code.

Event Loop and Asynchronous Model

Despite being single-threaded, JavaScript handles asynchronous tasks efficiently using a mechanism involving the Event Loop, Web APIs, and task queues. This allows JavaScript to remain responsive while performing operations like network requests, timers, and DOM events.

The Event Loop is the engine that coordinates this process. It continuously monitors the Call Stack and the task queues. When the Call Stack is empty, the Event Loop moves tasks from the queues to the stack, ensuring that asynchronous callbacks execute in the correct order.

How it works:

-> The Call Stack runs all synchronous code first.
-> Web APIs handle asynchronous operations, such as setTimeout, fetch, DOM events, or async functions. These tasks are processed independently of the main thread.
-> Once an asynchronous operation is complete, its callback is placed in one of two queues:

  • Microtask Queue – for Promises, async/await, and MutationObserver callbacks.
  • Macrotask (Callback) Queue – for setTimeout, setInterval, I/O tasks, and UI events.

-> The Event Loop checks if the Call Stack is empty.

  • If empty, it first pulls all microtasks from the Microtask Queue to the Call Stack.
  • Only after the microtasks are completed, a task from the macrotask (callback) queue is pushed onto the stack.

-> This cycle repeats continuously, keeping the JavaScript engine responsive while handling asynchronous tasks efficiently.

Key points:

  • Microtasks have priority over macrotasks; they always execute first once the stack is empty.
  • Even a setTimeout with 0ms delay waits until all synchronous code and microtasks complete.
  • The Event Loop ensures JavaScript remains non-blocking despite being single-threaded.

Code:

console.log("Start");

setTimeout(() => {
  console.log("Macrotask: Timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Microtask: Promise");
});

console.log("End");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
End
Microtask: Promise
Macrotask: Timeout

Memory Mnagement and Garbage Collection

JavaScript automatically handles memory. It keeps track of variables and objects, and frees memory when they are no longer needed.The memory is stored in as two parts:

1. stack:
The stack stores primitive values (numbers, strings, booleans) and references to functions. It works in a Last-In-First-Out (LIFO) manner, meaning values are pushed when functions are called and popped when they return. Stack memory is small, fast, and used for simple, short-lived data.

2. Heap:
The heap stores objects, arrays, and complex data structures. Unlike the stack, heap memory is unstructured and accessed via references. Objects in the heap remain as long as there is a reference to them, and they are larger and slower than stack memory.

Garbage Collection:
JavaScript automatically frees memory that is no longer reachable. The most common method is Mark-and-Sweep: the engine starts from roots (like global objects and the call stack), marks all reachable objects, and reclaims memory used by unreachable objects. This prevents memory leaks and ensures efficient memory usage.

Example:

let name = "Alice"; // stored in Stack
let user = { age: 25 }; // object stored in Heap, reference in Stack

function greet() {
  let message = "Hello"; // stored in Stack
  console.log(message);
}

greet();
user = null; // object in Heap becomes unreachable and eligible for GC
Enter fullscreen mode Exit fullscreen mode

Explanation:
Primitives like nameare stored on the stack, while objects like userare in the heap with a reference in the stack. Setting user = null removes the reference, making the heap object unreachable and ready for garbage collection.

Engine Optimizations - Why Javascript is so fast

Modern engines like V8 have advanced optimizers that continuously monitor, recompile, and optimize code while it runs.

Here's how it works:

1. Hidden Classes
When you create an object, V8 dynamically assigns it a hidden class that defines its structure.
If you add properties consistently (in the same order), the engine can optimize access patterns dramatically.

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const p1 = new Person('Alice', 25); // Fast
Enter fullscreen mode Exit fullscreen mode

But if you later add new properties dynamically or in a different order, the engine deoptimizes the object, leading to slower property access.

2. Inline Caching
The engine remembers how functions are used and assumes similar patterns in future calls. This cached assumption boosts speed, as re-checking property lookups becomes unnecessary.

function greet(person) {
  return person.name;
}
greet({ name: 'Tom' });
greet({ name: 'Jerry' }); // Faster after inline cache warm-up
Enter fullscreen mode Exit fullscreen mode

3. Hot Path Optimization
V8 detects “hot” (frequently executed) code and recompiles it into optimized machine code to make execution near-native fast. If assumptions break, it safely “deoptimizes” that section back to normal.

4. Constant Folding & Dead Code Elimination
During JIT compilation, V8 simplifies constant expressions (2 + 3 → 5) and removes unused code for efficiency.

5. Garbage Collection Enhancements
Modern engines use incremental and generational garbage collection, allowing smooth memory management without noticeable performance drops.

Wrapping It All Up

Let's recap the inner workings of javascript:

  • Engine reads and prepares code for execution.
  • Global Execution Context starts your program.
  • Hoisting pulls declarations up for organized execution.
  • Temporal Dead Zone (TDZ) ensures safe variable access.
  • Call Stack manages functions in sequence.
  • Heap stores dynamic data.
  • Event Loop orchestrates asynchronous behavior seamlessly.
  • JIT and Engine Optimizations keep performance lightning fast.

JavaScript might appear simple, but it runs on a sophisticated runtime that efficiently handles concurrency, memory, and performance optimizations. That’s why calling it “The Language Behind the Web” fits perfectly — every click, animation, and API call online flows through this system.

If you found this helpful, don’t forget to like, comment, and follow to see more blogs. Let’s keep learning together. Happy Coding!

Top comments (1)

Collapse
 
onlineproxy profile image
OnlineProxy

Modern JS engines are tiered: they parse to an AST, run bytecode, and JIT hot paths to machine code-so “interpreted” is kinda incomplete. V8, SpiderMonkey, and JavaScriptCore act pretty similar for most apps, ESM’s static graph helps cold-start and bundling, while CJS’s runtime resolution can slow things down. Event loop order goes sync - microtasks - requestAnimationFrame - macrotasks and Node tosses in process.nextTick and setImmediate for extra spice. async/await can quietly serialize work-reach for Promise.all, wire up cancellation via AbortController, and keep an eye on GC pauses/leaks with heap snapshots and WeakMap.