Browser Internals: A Senior Engineer's Deep Dive

Published: (January 11, 2026 at 01:52 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Multi‑Process Architecture

┌─────────────────────────────────────────────────────────────┐
│                     Browser Process                        │
│  (UI, bookmarks, network, storage)                          │
└─────────────────────────────────────────────────────────────┘
         │              │              │              │
         ▼              ▼              ▼              ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│  Renderer   │ │  Renderer   │ │  Renderer   │ │    GPU      │
│  Process    │ │  Process    │ │  Process    │ │  Process    │
│  (Tab 1)    │ │  (Tab 2)    │ │  (Tab 3)    │ │             │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
BenefitExplanation
SecurityEach tab is sandboxed; a malicious site can’t access other tabs.
StabilityIf one tab crashes, the others survive.
PerformanceParallel processing across CPU cores.

Key takeaway: This is the most important concept for frontend performance.

Rendering Pipeline

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│   HTML   │───▶│   DOM    │───▶│  Render  │───▶│  Layout  │───▶│  Paint   │
│  Parse   │    │   Tree   │    │   Tree   │    │          │    │          │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘
                     │                │
                     │                │
               ┌─────▼─────┐          │
               │   CSSOM   │──────────┘
               │   Tree    │
               └───────────┘

Example HTML

<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <div id="app">
      <p>Hello</p>
    </div>
  </body>
</html>

DOM Tree (simplified)

document

 html

 body

 div#app

  p

 "Hello"

Key Point: The parser is synchronous. When it encounters a “ tag, it stops parsing until the script is executed.

Sample CSS

body { font-size: 16px; }
#app { color: blue; }
p { margin: 10px; }

CSSOM (simplified)

CSSOM

 ┌────┴────┐
body      #app
(font:16) (color:blue)

  p
(margin:10)

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

Render Tree (only visible elements)

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

Not included in the render tree:

  • “ and its children
  • Elements with display: none
  • , , “

Layout (calculates exact positions & sizes)

┌────────────────────────────────────────┐
│ body: 0,0 – 1920x1080                 │
│  ┌──────────────────────────────────┐ │
│  │ div#app: 8,8 – 1904x500          │ │
│  │  ┌────────────────────────────┐ │ │
│  │  │ p: 8,18 – 1904x20          │ │ │
│  │  └────────────────────────────┘ │ │
│  └──────────────────────────────────┘ │
└────────────────────────────────────────┘

Expensive operation: Changing width, height, or position triggers a reflow of all descendants.

Paint (fills in pixels)

Paint order

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

The GPU then composites layers into the final image. Elements on separate layers can animate without repaint.

JavaScript Execution Model

┌─────────────────────────────────────────────────────────────┐
│                         HEAP                                 │
│                   (Object Storage)                           │
└─────────────────────────────────────────────────────────────┘

┌─────────────┐     ┌─────────────────────────────────────────┐
│   CALL      │     │              WEB APIs                  │
│   STACK     │     │ (setTimeout, fetch, DOM events, etc.) │
│             │     └──────────────────┬──────────────────┘
│ function()  │                        │
│ function()  │                        ▼
│ main()      │     ┌─────────────────────────────────────────┐
└─────────────┘     │           CALLBACK QUEUES                │
       ▲            │  ┌─────────────────────────────────────┐ │
       │            │  │ Microtask Queue (Promises, queueMicrotask) │ │
       │            │  └─────────────────────────────────────┘ │
       │            │  ┌─────────────────────────────────────┐ │
       └────────────│  │ Macrotask Queue (setTimeout, I/O)   │ │
     Event Loop    │  └─────────────────────────────────────┘ │
     picks next    └─────────────────────────────────────────────┘

Example

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

The Rule

  1. Execute all synchronous code (call stack empties).
  2. Execute all microtasks (Promise callbacks, queueMicrotask).
  3. Execute one macrotask (e.g., setTimeout, setInterval, I/O).
  4. Repeat from step 2.

Task Types Overview

MicrotasksMacrotasks
Promise.then / catch / finallysetTimeout
queueMicrotask()setInterval
MutationObserversetImmediate (Node)
process.nextTick (Node)
I/O callbacks
requestAnimationFrame*

* requestAnimationFrame runs before repaint, after microtasks.

Yielding to 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 (let i = 0; i < items.length; i++) {
    heavyWork(items[i]);
    await new Promise(r => setTimeout(r, 0));
  }
}

Understanding what triggers each type of task is crucial for performance.

Layout‑Triggering vs. Paint‑Triggering CSS Changes

Paint‑only changes (no layout):

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

Layout‑triggering changes (reflow):

element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.position = 'absolute';

CSS & Layout

le.padding = '10px';
element.style.margin = '20px';
element.style.display = 'none';          // Removed from layout
element.style.position = 'absolute';
element.style.fontSize = '20px';          // Text reflow!

The Worst Performance Anti‑Pattern

BAD – forces 100 reflows

// 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

// 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
});

Layout‑Triggering Getters (force an immediate reflow)

  • element.offsetTop / offsetLeft / offsetWidth / offsetHeight
  • element.scrollTop / scrollLeft / scrollWidth / scrollHeight
  • element.clientTop / clientLeft / clientWidth / clientHeight
  • element.getBoundingClientRect()
  • window.getComputedStyle(element)

GPU‑Friendly Animations

/* These animate on the GPU — 60 fps guaranteed */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
opacity: 0.5;

Modern Way

.animated-element {
  will-change: transform;
}

Legacy Fallback

.animated-element {
  transform: translateZ(0);  /* “Null transform hack” */
}

Avoid Overusing will-change

/* 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 */
}

Timing & the Paint Cycle

// BAD: Timer doesn’t sync with display refresh
setInterval(() => {
  element.style.left = x++ + 'px';
}, 16);  // Hoping for 60 fps
// GOOD: Synced with browser’s paint cycle
function animate() {
  element.style.left = x++ + 'px';
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

One Frame (~16.67 ms)

┌────────────────────────────────────────────────────────────┐
│                    One Frame (~16.67ms)                     │
├──────────┬──────────┬──────────┬──────────┬───────────────┤
│   JS     │   rAF    │  Style   │  Layout  │     Paint     │
│ (events) │ callbacks│  Calc    │          │   Composite   │
└──────────┴──────────┴──────────┴──────────┴───────────────┘

Off‑Loading Heavy Computation

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);
};

API Access Matrix

APICan AccessCannot Access
fetch
DOM
setTimeout/setInterval
window
WebSockets
document
IndexedDB
UI‑related APIs
postMessage
localStorage (use IndexedDB)

Common Memory‑Leak Patterns

  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(1_000_000);
      return () => console.log(largeData.length);
    }
  3. Detached DOM trees

    const div = document.createElement('div');
    div.innerHTML = 'Hello';
    // div never added to DOM, but JavaScript holds reference

Tip: Use Chrome DevTools → MemoryTake Heap Snapshot and compare snapshots before/after suspected leaks.

Garbage Collection Overview

  1. Mark Phase – Start from “roots” (global objects, stack) and mark all reachable objects.
  2. Sweep Phase – Delete all unmarked objects.

Summary (in my own words)

I understand the browser as a multi‑stage pipeline: parsing HTML/CSS into trees, merging them into a render tree, calculating layout, painting pixels, and compositing layers. I optimise by avoiding layout thrashing (batch reads before writes), using compositor‑friendly properties (transform, opacity) for animations, and leveraging requestAnimationFrame for smooth 60 fps. For heavy computation I off‑load work to Web Workers to keep the main thread responsive. Knowing the event loop—especially the micro‑task vs. macro‑task distinction—helps me write predictable asynchronous code.

Back to Blog

Related posts

Read more »