Skip to content

Browser Internals

Interactive DOM Visualizer

Explore the DOM tree, CSS box model, and event propagation interactively.

DOM Visualizer

Explore DOM tree structure, event bubbling, and the CSS box model

Explore the Document Object Model tree structure

DOM Tree
<html>
<head>
<body>
element
text
attribute
void
HTML Source
<html>
  <head>
    <title>My Page</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <header id="header">
      <h1 class="title">Welcome</h1>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
    </header>
    <main id="content">
      <h2>Article Title</h2>
      <p class="intro">This is an introduction paragraph.</p>
      <div class="card">
        <img src="photo.jpg" alt="A photo" />
        <p>Card description text.</p>
        <button id="btn">Click Me</button>
      </div>
    </main>
    <footer>
      <p>Copyright 2024</p>
    </footer>
  </body>
</html>

Understanding how browsers work under the hood is essential for writing performant web applications. This page explores the browser’s architecture, the critical rendering path, JavaScript engine internals, and the worker APIs that enable multithreading on the web.


Browser Architecture

A modern browser is a complex multi-process application. Here is how the major components are organized:

┌──────────────────────────────────────────────────────────┐
│ BROWSER PROCESS │
│ (UI, bookmarks, network coordination, storage) │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ RENDERER │ │ RENDERER │ │ RENDERER │ │
│ │ PROCESS │ │ PROCESS │ │ PROCESS │ │
│ │ (Tab 1) │ │ (Tab 2) │ │ (Tab 3) │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │ Blink │ │ │ │ Blink │ │ │ │ Blink │ │ │
│ │ │ (HTML/ │ │ │ │ (HTML/ │ │ │ │ (HTML/ │ │ │
│ │ │ CSS) │ │ │ │ CSS) │ │ │ │ CSS) │ │ │
│ │ ├─────────┤ │ │ ├─────────┤ │ │ ├─────────┤ │ │
│ │ │ V8 │ │ │ │ V8 │ │ │ │ V8 │ │ │
│ │ │ (JS) │ │ │ │ (JS) │ │ │ │ (JS) │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ GPU PROCESS │ │ NETWORK │ │
│ │ (compositing │ │ SERVICE │ │
│ │ and drawing)│ │ (HTTP, DNS, │ │
│ │ │ │ cache) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ PLUGIN │ │ STORAGE │ │
│ │ PROCESS │ │ PROCESS │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘

Major Browser Engines

BrowserRendering EngineJS Engine
ChromeBlinkV8
FirefoxGeckoSpiderMonkey
SafariWebKitJavaScriptCore (Nitro)
EdgeBlink (since 2020)V8

Process Isolation Benefits

  • Security — One tab’s renderer process cannot access another tab’s memory
  • Stability — A crashing tab does not bring down the entire browser
  • Performance — Each tab can utilize its own CPU core
  • Sandboxing — Renderer processes run with limited OS-level privileges

The Critical Rendering Path

The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on the screen.

HTML Document
┌───────────────────────┐
│ HTML Parser │
│ (Tokenizer → Tree │
│ Construction) │
└───────────┬───────────┘
┌──────────────┼──────────────┐
▼ │ ▼
┌─────────────┐ │ ┌─────────────────┐
│ DOM │ │ │ CSS Parser │
│ (Document │ │ │ (Tokenize → │
│ Object │ │ │ CSSOM) │
│ Model) │ │ └────────┬────────┘
└──────┬──────┘ │ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ JavaScript │ │
│ │ Engine │ │
│ │ (may modify │ │
│ │ DOM or CSSOM) │ │
│ └───────────────────┘ │
│ │
└──────────┬───────────────────┘
┌───────────────┐
│ Render Tree │
│ (visible │
│ nodes only) │
└───────┬───────┘
┌───────────────┐
│ Layout │
│ (calculate │
│ geometry) │
└───────┬───────┘
┌───────────────┐
│ Paint │
│ (rasterize │
│ pixels) │
└───────┬───────┘
┌───────────────┐
│ Composite │
│ (layer │
│ composition) │
└───────────────┘

Step-by-Step Breakdown

1. DOM Construction

The browser parses HTML bytes into tokens, then constructs the DOM tree:

Bytes → Characters → Tokens → Nodes → DOM
<html> document
<body> → └── html
<h1>Hello</h1> └── body
<p>World</p> ├── h1
</body> │ └── "Hello"
</html> └── p
└── "World"

2. CSSOM Construction

CSS is parsed into the CSS Object Model — a tree that mirrors the DOM but contains computed style information:

body { font-size: 16px; } CSSOM:
h1 { font-weight: bold; } └── body
p { color: gray; } │ font-size: 16px
├── h1
│ font-size: 16px (inherited)
│ font-weight: bold
└── p
font-size: 16px (inherited)
color: gray

CSS is render-blocking — the browser will not render any content until it has built the CSSOM for all stylesheets.

3. Render Tree

The render tree combines the DOM and CSSOM, containing only visible elements:

DOM CSSOM Render Tree
html html body
├── head ├── head ├── h1 "Hello"
│ └── style │ └── (hidden) │ font-weight: bold
├── body └── body └── p "World"
│ ├── h1 "Hello" ├── h1 color: gray
│ ├── p "World" └── p
│ └── span (hidden)
│ display: none (excluded from render tree)

Elements with display: none are excluded. Elements with visibility: hidden are included (they still affect layout).

4. Layout (Reflow)

The browser calculates the exact position and size of every element in the render tree:

Viewport: 800px wide
body: x=0, y=0, width=800, height=auto
h1: x=0, y=0, width=800, height=38
p: x=0, y=46, width=800, height=24

5. Paint

The browser fills in pixels — colors, images, borders, text, shadows:

Paint operations:
1. Background color of body
2. Background color of h1
3. Text "Hello" in h1
4. Background color of p
5. Text "World" in p (gray)
6. Borders, shadows, etc.

6. Composite

The browser combines painted layers into the final screen image. Elements with transforms, opacity, or will-change get their own compositor layers.


Reflow vs Repaint

Understanding the difference between reflow and repaint is critical for performance.

Reflow (Layout)Repaint
TriggerGeometry changes (size, position)Appearance changes (color, shadow)
CostVery expensive (recalculates layout)Moderate (re-rasterizes affected area)
ScopeCan cascade to parent/child elementsOnly affected elements
Exampleswidth, height, margin, padding, display, position, font-sizecolor, background, box-shadow, border-color, visibility

What Triggers Reflow

// Properties that trigger layout (reflow)
element.offsetHeight; // Reading layout properties forces reflow
element.getBoundingClientRect();
element.scrollTop;
element.clientWidth;
window.getComputedStyle(element);
// Changes that trigger reflow
element.style.width = '200px';
element.style.margin = '10px';
element.style.display = 'block';
element.style.fontSize = '18px';
element.className = 'new-class'; // if it changes layout
element.appendChild(newChild);
element.innerHTML = '...';
window.resizeTo(width, height);

What Triggers Only Repaint

// Changes that trigger repaint (not reflow)
element.style.color = 'red';
element.style.backgroundColor = '#f0f0f0';
element.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
element.style.borderColor = 'blue';
element.style.visibility = 'hidden'; // still occupies space
element.style.outline = '2px solid red';

What Triggers Neither (Composite Only)

// These only affect compositing — cheapest visual updates
element.style.transform = 'translateX(100px)';
element.style.opacity = 0.5;
element.style.willChange = 'transform'; // promotes to own layer

Layout Thrashing

Layout thrashing occurs when you rapidly alternate between reading and writing layout properties:

// BAD: layout thrashing (forces reflow on every read)
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
const height = el.offsetHeight; // read → forces reflow
el.style.height = (height * 2) + 'px'; // write → invalidates layout
// Next iteration: read again → forces another reflow!
});
// GOOD: batch reads, then batch writes
const heights = [];
elements.forEach(el => {
heights.push(el.offsetHeight); // batch all reads first
});
elements.forEach((el, i) => {
el.style.height = (heights[i] * 2) + 'px'; // then batch writes
});
// BEST: use requestAnimationFrame
function updateLayout() {
// Reads
const height = element.offsetHeight;
requestAnimationFrame(() => {
// Writes (in the next frame)
element.style.height = (height * 2) + 'px';
});
}

V8 Engine Basics

V8 is Google’s open-source JavaScript engine, powering Chrome and Node.js. Here is how it executes your code:

JavaScript Source Code
┌──────────────────┐
│ Parser │
│ (Tokenizer → │
│ AST) │
└────────┬─────────┘
┌──────────────────┐
│ Ignition │
│ (Interpreter) │
│ Generates │
│ bytecode │
└────────┬─────────┘
│ Hot functions detected
│ (execution count threshold)
┌──────────────────┐
│ TurboFan │
│ (Optimizing │
│ Compiler) │
│ Generates │
│ machine code │
└────────┬─────────┘
│ Deoptimization
│ (type assumptions
│ violated)
Back to Ignition

Key Concepts

Just-In-Time (JIT) Compilation — V8 does not simply interpret JavaScript. It compiles hot code paths to optimized machine code at runtime.

Hidden Classes — V8 creates internal “shapes” for objects to enable fast property access, similar to how statically-typed languages work:

// V8 can optimize this — consistent shape
function createPoint(x, y) {
return { x, y }; // always same shape: { x: number, y: number }
}
// V8 cannot optimize this well — inconsistent shapes
function createUser(name, opts) {
const user = { name };
if (opts.email) user.email = opts.email; // sometimes present
if (opts.age) user.age = opts.age; // sometimes present
return user; // shape varies per call
}

Inline Caches — V8 caches the result of property lookups to avoid repeated hash map lookups:

// Monomorphic (one shape) — fast
function getX(point) {
return point.x; // always called with same-shaped objects
}
// Polymorphic (few shapes) — slower
function getName(obj) {
return obj.name; // called with User, Product, Order objects
}
// Megamorphic (many shapes) — slow
function getValue(obj) {
return obj.value; // called with dozens of different object shapes
}

Web Workers

Web Workers run JavaScript in a background thread, separate from the main thread. This prevents expensive computations from blocking the UI.

Main Thread (UI) Worker Thread
┌─────────────────┐ ┌─────────────────┐
│ DOM access │ ◀──messages──▶ │ No DOM access │
│ Event handling │ │ CPU-intensive │
│ Rendering │ │ computations │
│ User input │ │ Data processing │
└─────────────────┘ └─────────────────┘

Dedicated Workers

main.js
const worker = new Worker('worker.js');
// Send data to worker
worker.postMessage({
type: 'PROCESS_DATA',
data: largeDataset
});
// Receive results from worker
worker.addEventListener('message', (event) => {
console.log('Result:', event.data);
updateUI(event.data);
});
// Handle errors
worker.addEventListener('error', (event) => {
console.error('Worker error:', event.message);
});
// Transfer ownership of ArrayBuffer (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage({ buffer }, [buffer]);
// buffer is now empty in the main thread — ownership transferred
// Terminate worker
worker.terminate();

Shared Workers

Shared Workers can be accessed by multiple pages from the same origin:

// Any page on the same origin
const shared = new SharedWorker('shared-worker.js');
shared.port.start();
shared.port.postMessage('Hello from page');
shared.port.addEventListener('message', (e) => {
console.log('Response:', e.data);
});
// shared-worker.js
const connections = [];
self.addEventListener('connect', (e) => {
const port = e.ports[0];
connections.push(port);
port.addEventListener('message', (event) => {
// Broadcast to all connected pages
connections.forEach(conn => conn.postMessage(event.data));
});
port.start();
});

What Workers Can and Cannot Do

Can AccessCannot Access
fetch / XMLHttpRequestDOM (document, window)
WebSocketdocument.querySelector
IndexedDBlocalStorage / sessionStorage
setTimeout / setIntervalalert / confirm / prompt
Crypto APIDirect UI manipulation
Cache APIParent page’s variables

Service Workers

Service Workers are a special type of worker that acts as a programmable network proxy between the browser and the network. They enable offline support, background sync, and push notifications.

Normal Request Flow:
Browser ──────────────────▶ Network ──▶ Server
With Service Worker:
Browser ──▶ Service Worker ──▶ Network ──▶ Server
Cache Storage
(offline support)

Lifecycle

┌──────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐
│ Register │ ──▶ │ Install │ ──▶ │ Activate │ ──▶ │ Active │
│ │ │ (cache │ │ (clean up │ │ (handles │
│ │ │ assets) │ │ old │ │ fetch │
│ │ │ │ │ caches) │ │ events) │
└──────────┘ └───────────┘ └───────────┘ └──────────┘
│ │
│ waitUntil() │
│ prevents activation │
│ until caching complete │

Implementation

// main.js — register the service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register(
'/sw.js',
{ scope: '/' }
);
console.log('SW registered:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// New version available — prompt user to refresh
showUpdateNotification();
}
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}

Caching Strategies

StrategyDescriptionUse Case
Cache FirstCheck cache first, then networkStatic assets (images, fonts, CSS)
Network FirstTry network first, fall back to cacheAPI data, HTML pages
Stale While RevalidateReturn cache immediately, update in backgroundNews feeds, non-critical data
Network OnlyAlways use network (skip cache)Analytics, non-cacheable requests
Cache OnlyOnly use cache (no network)Offline-only resources
// Stale-While-Revalidate implementation
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// Return cached immediately, update cache in background
return cachedResponse || fetchPromise;
});
})
);
});

Script Loading Strategies

How you load JavaScript affects the critical rendering path:

Normal <script>:
HTML: ──parse──│ blocked │──parse──
JS: │─download─│─execute─│
<script async>:
HTML: ──parse──────parse──│blocked│──parse──
JS: │─download──────│execute│
<script defer>:
HTML: ──parse──────────────parse──────│
JS: │─download──────────────────│execute│
DOMContentLoaded
<!-- Blocks HTML parsing (avoid in <head>) -->
<script src="app.js"></script>
<!-- Downloads in parallel, executes ASAP (order not guaranteed) -->
<script async src="analytics.js"></script>
<!-- Downloads in parallel, executes after HTML parsed (order preserved) -->
<script defer src="app.js"></script>
<script defer src="vendor.js"></script>
<!-- Module scripts are deferred by default -->
<script type="module" src="app.mjs"></script>
<!-- Preload critical resources -->
<link rel="preload" href="critical.js" as="script" />
<link rel="preload" href="hero.webp" as="image" />
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
<!-- Prefetch resources for future navigation -->
<link rel="prefetch" href="/next-page.js" />
<!-- Preconnect to external origins -->
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />

Rendering Performance Checklist

CategoryOptimization
Critical PathInline critical CSS, defer non-critical CSS, use async/defer for scripts
LayoutAvoid layout thrashing, batch DOM reads/writes, use transform instead of top/left
PaintMinimize paint areas, promote heavy animations to own layer with will-change
CompositeUse transform and opacity for animations (compositor-only, off main thread)
ImagesSet explicit width/height, use loading="lazy", serve modern formats (WebP/AVIF)
FontsUse font-display: swap, preload critical fonts, use system font stack as fallback
JavaScriptCode-split, tree-shake, defer non-critical, move work to Web Workers