浏览器内部:资深工程师的深度解析
Source: Dev.to
多进程架构
┌─────────────────────────────────────────────────────────────┐
│ Browser Process │
│ (UI, bookmarks, network, storage) │
└─────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Renderer │ │ Renderer │ │ Renderer │ │ GPU │
│ Process │ │ Process │ │ Process │ │ Process │
│ (Tab 1) │ │ (Tab 2) │ │ (Tab 3) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
| 好处 | 说明 |
|---|---|
| 安全性 | 每个标签页都在沙箱中运行;恶意站点无法访问其他标签页。 |
| 稳定性 | 如果一个标签页崩溃,其他标签页仍然可以继续工作。 |
| 性能 | 在 CPU 核心之间实现并行处理。 |
关键要点: 这是前端性能中最重要的概念。
Source: …
渲染管线
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ HTML │───▶│ DOM │───▶│ Render │───▶│ Layout │───▶│ Paint │
│ Parse │ │ Tree │ │ Tree │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │
│ │
┌─────▼─────┐ │
│ CSSOM │──────────┘
│ Tree │
└───────────┘
示例 HTML
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
<div id="app">
<p>Hello</p>
</div>
</body>
</html>
DOM 树(简化版)
document
│
html
│
body
│
div#app
│
p
│
"Hello"
关键点: 解析器是同步的。当它遇到 “ 标签时,会 停止 解析,直到脚本执行完毕。
示例 CSS
body { font-size: 16px; }
#app { color: blue; }
p { margin: 10px; }
CSSOM(简化版)
CSSOM
│
┌────┴────┐
body #app
(font:16) (color:blue)
│
p
(margin:10)
关键点: 构建 CSSOM 会阻塞渲染。这就是我们内联 关键 CSS 的原因。
渲染树(仅包含可见元素)
Render Tree:
body (font: 16px)
└─ div#app (color: blue)
└─ p (margin: 10px)
└─ "Hello"
渲染树中不包含的内容:
- “ 及其子节点
display: none的元素、、“
布局(计算精确的位置和尺寸)
┌────────────────────────────────────────┐
│ body: 0,0 – 1920x1080 │
│ ┌──────────────────────────────────┐ │
│ │ div#app: 8,8 – 1904x500 │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ p: 8,18 – 1904x20 │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
耗费资源的操作: 改变宽度、高度或位置会触发所有后代的 回流(reflow)。
绘制(填充像素)
绘制顺序
- 背景颜色
- 背景图像
- 边框
- 子元素
- 轮廓
随后 GPU 将各层合成为最终图像。位于不同层的元素可以在不重新绘制的情况下进行动画。
Source:
JavaScript 执行模型
┌─────────────────────────────────────────────────────────────┐
│ 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 └─────────────────────────────────────────────┘
示例
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
// 输出: 1, 4, 3, 2
规则
- 执行所有同步代码(调用栈清空)。
- 执行 所有 微任务(Promise 回调、
queueMicrotask)。 - 执行 一个 宏任务(例如
setTimeout、setInterval、I/O)。 - 从步骤 2 重新开始循环。
任务类型概览
| 微任务 | 宏任务 |
|---|---|
Promise.then / catch / finally | setTimeout |
queueMicrotask() | setInterval |
MutationObserver | setImmediate(Node) |
process.nextTick(Node) | |
| I/O 回调 | |
requestAnimationFrame* |
* requestAnimationFrame 在 重绘前、微任务之后执行。
让出事件循环
// BAD: 阻塞 5 秒
function processLargeArray(items) {
items.forEach(item => {
// 重度计算
heavyWork(item);
});
}
// GOOD: 让出事件循环
async function processLargeArray(items) {
for (let i = 0; i < items.length; i++) {
heavyWork(items[i]);
await new Promise(r => setTimeout(r, 0));
}
}
了解触发每种任务的原因对性能至关重要。
触发布局 vs. 触发绘制的 CSS 更改
仅绘制更改(不触发布局):
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.visibility = 'hidden'; // Still takes space
element.style.opacity = 0.5;
触发布局的更改(回流):
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.position = 'absolute';
CSS 与布局
le.padding = '10px';
element.style.margin = '20px';
element.style.display = 'none'; // Removed from layout
element.style.position = 'absolute';
element.style.fontSize = '20px'; // Text reflow!
最差的性能反模式
错误 – 强制 100 次回流
// 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
});
布局触发获取器(强制立即回流)
element.offsetTop/offsetLeft/offsetWidth/offsetHeightelement.scrollTop/scrollLeft/scrollWidth/scrollHeightelement.clientTop/clientLeft/clientWidth/clientHeightelement.getBoundingClientRect()window.getComputedStyle(element)
GPU‑友好的动画
/* These animate on the GPU — 60 fps guaranteed */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
opacity: 0.5;
现代写法
.animated-element {
will-change: transform;
}
传统回退方案
.animated-element {
transform: translateZ(0); /* “Null transform hack” */
}
避免过度使用 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 */
}
计时与绘制周期
// BAD: 计时器未与显示刷新同步
setInterval(() => {
element.style.left = x++ + 'px';
}, 16); // 希望达到 60 fps
// GOOD: 与浏览器的绘制周期同步
function animate() {
element.style.left = x++ + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
一帧(约 16.67 ms)
┌────────────────────────────────────────────────────────────┐
│ 一帧(约 16.67 ms) │
├──────────┬──────────┬──────────┬──────────┬───────────────┤
│ JS(事件) │ rAF 回调│ 样式计算 │ 布局 │ 绘制 合成 │
└──────────┴──────────┴──────────┴──────────┴───────────────┘
卸载繁重计算
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 访问矩阵
| API | 可访问 | 不可访问 |
|---|---|---|
fetch | ✅ | |
DOM | ✅ | |
setTimeout/setInterval | ✅ | |
window | ✅ | |
WebSockets | ✅ | |
document | ✅ | |
IndexedDB | ✅ | |
| UI‑相关 API | ✅ | |
postMessage | ✅ | |
localStorage(使用 IndexedDB) | ✅ |
常见内存泄漏模式
-
忘记移除事件监听器
element.addEventListener('click', handler); // element removed from DOM, but handler still references it -
闭包持有引用
function createHandler() { const largeData = new Array(1_000_000); return () => console.log(largeData.length); } -
已分离的 DOM 树
const div = document.createElement('div'); div.innerHTML = 'Hello'; // div never added to DOM, but JavaScript holds reference
提示: 使用 Chrome DevTools → Memory → Take Heap Snapshot,并比较疑似泄漏前后的快照。
垃圾回收概述
- Mark Phase – 从“根”(全局对象、栈)开始标记所有可达对象。
- Sweep Phase – 删除所有未标记的对象。
摘要(用我自己的话)
我把浏览器理解为一个多阶段管线:将 HTML/CSS 解析成树结构,将它们合并为渲染树,计算布局,绘制像素,并合成图层。通过避免布局抖动(先批量读取再写入),使用对合成友好的属性(
transform、opacity)进行动画,并利用requestAnimationFrame实现平滑的 60 fps。我会将繁重的计算任务卸载到 Web Workers,以保持主线程的响应性。了解事件循环——尤其是微任务与宏任务的区别——帮助我编写可预测的异步代码。