Browser Internals: A Senior Engineer's Deep Dive
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) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
| Benefit | Explanation |
|---|---|
| Security | Each tab is sandboxed; a malicious site can’t access other tabs. |
| Stability | If one tab crashes, the others survive. |
| Performance | Parallel 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
- Background color
- Background image
- Border
- Children
- 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
- Execute all synchronous code (call stack empties).
- Execute all microtasks (Promise callbacks,
queueMicrotask). - Execute one macrotask (e.g.,
setTimeout,setInterval, I/O). - Repeat from step 2.
Task Types Overview
| 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.
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/offsetHeightelement.scrollTop/scrollLeft/scrollWidth/scrollHeightelement.clientTop/clientLeft/clientWidth/clientHeightelement.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
| API | Can Access | Cannot Access |
|---|---|---|
fetch | ✅ | |
DOM | ✅ | |
setTimeout/setInterval | ✅ | |
window | ✅ | |
WebSockets | ✅ | |
document | ✅ | |
IndexedDB | ✅ | |
| UI‑related APIs | ✅ | |
postMessage | ✅ | |
localStorage (use IndexedDB) | ✅ |
Common Memory‑Leak Patterns
-
Forgotten event listeners
element.addEventListener('click', handler); // element removed from DOM, but handler still references it -
Closures holding references
function createHandler() { const largeData = new Array(1_000_000); return () => console.log(largeData.length); } -
Detached DOM trees
const div = document.createElement('div'); div.innerHTML = 'Hello'; // div never added to DOM, but JavaScript holds reference
Tip: Use Chrome DevTools → Memory → Take Heap Snapshot and compare snapshots before/after suspected leaks.
Garbage Collection Overview
- Mark Phase – Start from “roots” (global objects, stack) and mark all reachable objects.
- 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 leveragingrequestAnimationFramefor 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.