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) β β β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
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 β
βββββββββββββ
Step-by-Step Breakdown
1. HTML Parsing β DOM Tree
<html>
<body>
<div id="app">
<p>Hello</p>
</div>
</body>
</html>
document
β
html
β
body
β
div#app
β
p
β
"Hello"
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; }
CSSOM
β
ββββββ΄βββββ
body #app
(font:16) (color:blue)
β
p
(margin:10)
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>
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 β β β
β β ββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββ
Expensive Operation: Changing width, height, position triggers reflow of all descendants.
5. Paint
Fills in pixels: colors, borders, shadows, text.
Paint Order:
- Background color
- Background image
- Border
- Children
- 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 βββββββββββββββββββββββββββββββββββββββββββ
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
The Rule:
- Execute all synchronous code (Call Stack empties)
- Execute ALL microtasks (Promise callbacks, queueMicrotask)
- Execute ONE macrotask (setTimeout, setInterval, I/O)
- 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));
}
}
}
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;
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!
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
});
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)
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;
How to Promote to Own Layer
/* Modern way */
.animated-element {
will-change: transform;
}
/* Legacy fallback */
.animated-element {
transform: translateZ(0); /* "Null transform hack" */
}
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 */
}
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);
When rAF Fires
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β One Frame (~16.67ms) β
ββββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββ¬ββββββββββββββββ€
β JS β rAF β Style β Layout β Paint β
β (events) βcallbacks β Calc β β Composite β
ββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββ΄ββββββββββββββββ
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);
};
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
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
Detecting Leaks
// Chrome DevTools β Memory β Take Heap Snapshot
// Compare snapshots before and after suspected leak
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)