DEV Community

Cover image for Browser Internals: A Senior Engineer's Deep Dive
Arghya Majumder
Arghya Majumder

Posted on

Browser Internals: A Senior Engineer's Deep Dive

Browser Internals: A Senior Engineer's Deep Dive

Understanding how the browser works under the hood is essential for performance optimization and debugging.


1. The Browser Architecture

Modern browsers have a multi-process architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Browser Process                          β”‚
β”‚  (UI, bookmarks, network, storage)                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚              β”‚              β”‚              β”‚
         β–Ό              β–Ό              β–Ό              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Renderer   β”‚ β”‚  Renderer   β”‚ β”‚  Renderer   β”‚ β”‚    GPU      β”‚
β”‚  Process    β”‚ β”‚  Process    β”‚ β”‚  Process    β”‚ β”‚  Process    β”‚
β”‚  (Tab 1)    β”‚ β”‚  (Tab 2)    β”‚ β”‚  (Tab 3)    β”‚ β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Why Multiple Processes?

Benefit Explanation
Security Each tab is sandboxed; malicious site can't access other tabs
Stability If one tab crashes, others survive
Performance Parallel processing across CPU cores

2. The Rendering Pipeline (Critical Rendering Path)

This is the most important concept for frontend performance.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   HTML   │───▢│   DOM    │───▢│  Render  │───▢│  Layout  │───▢│  Paint   β”‚
β”‚  Parse   β”‚    β”‚   Tree   β”‚    β”‚   Tree   β”‚    β”‚          β”‚    β”‚          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚                β”‚
                     β”‚                β”‚
               β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”          β”‚
               β”‚   CSSOM   β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚   Tree    β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Breakdown

1. HTML Parsing β†’ DOM Tree

<html>
  <body>
    <div id="app">
      <p>Hello</p>
    </div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
        document
            β”‚
          html
            β”‚
          body
            β”‚
        div#app
            β”‚
           p
            β”‚
        "Hello"
Enter fullscreen mode Exit fullscreen mode

Key Point: Parser is synchronous. When it hits <script>, it STOPS.

2. CSS Parsing β†’ CSSOM Tree

body { font-size: 16px; }
#app { color: blue; }
p { margin: 10px; }
Enter fullscreen mode Exit fullscreen mode
        CSSOM
          β”‚
     β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
   body      #app
(font:16)   (color:blue)
     β”‚
     p
 (margin:10)
Enter fullscreen mode Exit fullscreen mode

Key Point: CSSOM construction blocks rendering. This is why we inline critical CSS.

3. Render Tree (DOM + CSSOM)

Only visible elements are included:

Render Tree:
  body (font: 16px)
    └─ div#app (color: blue)
         └─ p (margin: 10px)
              └─ "Hello"

NOT included:
  - <head> and its children
  - Elements with display: none
  - <script>, <meta>, <link>
Enter fullscreen mode Exit fullscreen mode

4. Layout (Reflow)

Calculates the exact position and size of each element:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ body: 0,0 - 1920x1080                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ div#app: 8,8 - 1904x500          β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ p: 8,18 - 1904x20          β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Expensive Operation: Changing width, height, position triggers reflow of all descendants.

5. Paint

Fills in pixels: colors, borders, shadows, text.

Paint Order:

  1. Background color
  2. Background image
  3. Border
  4. Children
  5. Outline

6. Composite

GPU combines layers into final image. Elements on separate layers can animate without repaint.


3. The Event Loop: JavaScript's Heartbeat

JavaScript is single-threaded. The Event Loop is how it handles async operations.

The Mental Model

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         HEAP                                 β”‚
β”‚                   (Object Storage)                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   CALL      β”‚     β”‚              WEB APIs                    β”‚
β”‚   STACK     β”‚     β”‚  (setTimeout, fetch, DOM events, etc.)  β”‚
β”‚             β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  function() β”‚                        β”‚
β”‚  function() β”‚                        β–Ό
β”‚  main()     β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚           CALLBACK QUEUES                β”‚
       β–²            β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
       β”‚            β”‚  β”‚ Microtask Queue (Promises, queueMT) β”‚ β”‚
       β”‚            β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
       β”‚            β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
       └────────────│  β”‚ Macrotask Queue (setTimeout, I/O)   β”‚ β”‚
     Event Loop     β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
     picks next     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Execution Order

console.log('1');  // Sync

setTimeout(() => console.log('2'), 0);  // Macrotask

Promise.resolve().then(() => console.log('3'));  // Microtask

console.log('4');  // Sync

// Output: 1, 4, 3, 2
Enter fullscreen mode Exit fullscreen mode

The Rule:

  1. Execute all synchronous code (Call Stack empties)
  2. Execute ALL microtasks (Promise callbacks, queueMicrotask)
  3. Execute ONE macrotask (setTimeout, setInterval, I/O)
  4. Repeat from step 2

Microtasks vs Macrotasks

Microtasks Macrotasks
Promise.then/catch/finally setTimeout
queueMicrotask() setInterval
MutationObserver setImmediate (Node)
process.nextTick (Node) I/O callbacks
requestAnimationFrame*

*requestAnimationFrame runs before repaint, after microtasks.

The Danger: Blocking the Event Loop

// BAD: Blocks for 5 seconds
function processLargeArray(items) {
  items.forEach(item => {
    // Heavy computation
    heavyWork(item);
  });
}

// GOOD: Yield to the event loop
async function processLargeArray(items) {
  for (const item of items) {
    heavyWork(item);

    // Let browser breathe every 100 items
    if (index % 100 === 0) {
      await new Promise(r => setTimeout(r, 0));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Reflow vs Repaint

Understanding what triggers each is crucial for performance.

Repaint (Cheap)

Changes to visual properties that don't affect layout:

element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden';  // Still takes space
element.style.opacity = 0.5;
Enter fullscreen mode Exit fullscreen mode

Reflow (Expensive)

Changes to geometry trigger layout recalculation:

element.style.width = '100px';
element.style.height = '200px';
element.style.padding = '10px';
element.style.margin = '20px';
element.style.display = 'none';  // Removed from layout
element.style.position = 'absolute';
element.style.fontSize = '20px';  // Text reflow!
Enter fullscreen mode Exit fullscreen mode

Layout Thrashing

The worst performance anti-pattern:

// BAD: Forces 100 reflows!
elements.forEach(el => {
  const height = el.offsetHeight;  // READ β†’ forces layout
  el.style.height = height + 10 + 'px';  // WRITE β†’ invalidates layout
});

// GOOD: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight);  // All reads

elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px';  // All writes
});
Enter fullscreen mode Exit fullscreen mode

Properties That Trigger Layout

Reading these forces an immediate reflow:

// These are "layout-triggering" getters
element.offsetTop / offsetLeft / offsetWidth / offsetHeight
element.scrollTop / scrollLeft / scrollWidth / scrollHeight
element.clientTop / clientLeft / clientWidth / clientHeight
element.getBoundingClientRect()
window.getComputedStyle(element)
Enter fullscreen mode Exit fullscreen mode

5. Compositor Layers

The GPU can animate certain properties without reflow or repaint.

Properties Handled by Compositor

/* These animate on the GPU β€” 60fps guaranteed */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
opacity: 0.5;
Enter fullscreen mode Exit fullscreen mode

How to Promote to Own Layer

/* Modern way */
.animated-element {
  will-change: transform;
}

/* Legacy fallback */
.animated-element {
  transform: translateZ(0);  /* "Null transform hack" */
}
Enter fullscreen mode Exit fullscreen mode

The Layer Explosion Problem

/* BAD: Creates too many layers */
* {
  will-change: transform;
}

/* GOOD: Only elements that will animate */
.card:hover {
  will-change: transform;
}
.card {
  will-change: auto;  /* Release after animation */
}
Enter fullscreen mode Exit fullscreen mode

6. requestAnimationFrame: The Right Way to Animate

Why Not setTimeout?

// BAD: Timer doesn't sync with display refresh
setInterval(() => {
  element.style.left = x++ + 'px';
}, 16);  // Hoping for 60fps

// GOOD: Synced with browser's paint cycle
function animate() {
  element.style.left = x++ + 'px';
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
Enter fullscreen mode Exit fullscreen mode

When rAF Fires

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    One Frame (~16.67ms)                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   JS     β”‚   rAF    β”‚  Style   β”‚  Layout  β”‚     Paint     β”‚
β”‚ (events) β”‚callbacks β”‚  Calc    β”‚          β”‚   Composite   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

7. Web Workers: True Parallelism

For heavy computation that would block the main thread:

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ data: largeArray });

worker.onmessage = (event) => {
  console.log('Result:', event.data);
};

// worker.js
self.onmessage = (event) => {
  const result = heavyComputation(event.data);
  self.postMessage(result);
};
Enter fullscreen mode Exit fullscreen mode

Limitations

Can Access Cannot Access
fetch DOM
setTimeout/setInterval window
WebSockets document
IndexedDB UI-related APIs
postMessage localStorage (use IndexedDB)

8. Memory Management & Garbage Collection

How GC Works (Mark and Sweep)

1. Mark Phase: Start from "roots" (global, stack), mark all reachable objects
2. Sweep Phase: Delete all unmarked objects
Enter fullscreen mode Exit fullscreen mode

Common Memory Leaks

// 1. Forgotten event listeners
element.addEventListener('click', handler);
// element removed from DOM, but handler still references it

// 2. Closures holding references
function createHandler() {
  const largeData = new Array(1000000);
  return () => console.log(largeData.length);
}

// 3. Detached DOM trees
const div = document.createElement('div');
div.innerHTML = '<span>Hello</span>';
// div never added to DOM, but JavaScript holds reference
Enter fullscreen mode Exit fullscreen mode

Detecting Leaks

// Chrome DevTools β†’ Memory β†’ Take Heap Snapshot
// Compare snapshots before and after suspected leak
Enter fullscreen mode Exit fullscreen mode

9. Interview Tip

"I understand the browser as a multi-stage pipeline: parsing HTML/CSS into trees, combining them into the render tree, calculating layout, painting pixels, and compositing layers. I optimize by avoiding layout thrashing (batch reads before writes), using compositor-friendly properties (transform, opacity) for animations, and leveraging requestAnimationFrame for smooth 60fps. For heavy computation, I use Web Workers to keep the main thread responsive. Understanding the event loop β€” especially the microtask/macrotask distinction β€” helps me write predictable async code."

Top comments (0)