浏览器内部:资深工程师的深度解析

发布: (2026年1月12日 GMT+8 02:52)
8 min read
原文: Dev.to

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)。

绘制(填充像素)

绘制顺序

  1. 背景颜色
  2. 背景图像
  3. 边框
  4. 子元素
  5. 轮廓

随后 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

规则

  1. 执行所有同步代码(调用栈清空)。
  2. 执行 所有 微任务(Promise 回调、queueMicrotask)。
  3. 执行 一个 宏任务(例如 setTimeoutsetInterval、I/O)。
  4. 从步骤 2 重新开始循环。

任务类型概览

微任务宏任务
Promise.then / catch / finallysetTimeout
queueMicrotask()setInterval
MutationObserversetImmediate(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 / offsetHeight
  • element.scrollTop / scrollLeft / scrollWidth / scrollHeight
  • element.clientTop / clientLeft / clientWidth / clientHeight
  • element.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)

常见内存泄漏模式

  1. 忘记移除事件监听器

    element.addEventListener('click', handler);
    // element removed from DOM, but handler still references it
  2. 闭包持有引用

    function createHandler() {
      const largeData = new Array(1_000_000);
      return () => console.log(largeData.length);
    }
  3. 已分离的 DOM 树

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

提示: 使用 Chrome DevTools → MemoryTake Heap Snapshot,并比较疑似泄漏前后的快照。

垃圾回收概述

  1. Mark Phase – 从“根”(全局对象、栈)开始标记所有可达对象。
  2. Sweep Phase – 删除所有未标记的对象。

摘要(用我自己的话)

我把浏览器理解为一个多阶段管线:将 HTML/CSS 解析成树结构,将它们合并为渲染树,计算布局,绘制像素,并合成图层。通过避免布局抖动(先批量读取再写入),使用对合成友好的属性(transformopacity)进行动画,并利用 requestAnimationFrame 实现平滑的 60 fps。我会将繁重的计算任务卸载到 Web Workers,以保持主线程的响应性。了解事件循环——尤其是微任务与宏任务的区别——帮助我编写可预测的异步代码。

Back to Blog

相关文章

阅读更多 »