Skip to content

第 23 章 · 浏览器原理

一句话开篇:浏览器是前端的「家」——不理解它,写出来的代码就像不知道厨房布局的厨师做菜,永远只能照菜谱,做不出意外。


0. 生活类比(先建立直觉)

把浏览器想象成一家外卖中央厨房

浏览器组件中央厨房
浏览器进程总经理(管前台、调度)
网络进程采购员(去网上下单买原料)
渲染进程厨师(把原料做成成品 = 把 HTML 渲染成像素)
GPU 进程摆盘师(把菜摆得漂亮、上桌)
插件进程外聘的临时帮厨
标签页一个独立厨房(每个 tab 一个渲染进程)

把"打开一个网页"想象成"下单一份外卖":

你下单(输入 URL)

总经理(浏览器进程)记录订单

采购员(网络进程)跑去市场(DNS → TCP → HTTP)

厨师(渲染进程)拿到原料(HTML/CSS/JS)开始做菜

摆盘师(GPU 进程)摆好端给你(屏幕显示)

多进程的好处:一个厨房着火(一个标签页崩溃),不会烧掉整个外卖店(其他标签页)。这就是 Chrome 的核心设计。


1. 浏览器架构(多进程模型)

现代浏览器(以 Chrome 为代表)是多进程架构:

┌─────────────────────────────────────────────────────────────┐
│                   Chrome 多进程架构                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────┐      管理标签、地址栏、菜单            │
│  │  浏览器主进程     │      唯一一个                         │
│  └──────────────────┘                                       │
│           │                                                 │
│           ├──────────────────┐                              │
│           ↓                  ↓                              │
│  ┌──────────────────┐  ┌──────────────────┐                 │
│  │  渲染进程 1       │  │  渲染进程 2       │  每个标签一个 │
│  │ (taobao.com)     │  │ (zhihu.com)      │  (沙箱隔离)   │
│  └──────────────────┘  └──────────────────┘                 │
│                                                             │
│  ┌──────────────────┐  ┌──────────────────┐                 │
│  │  网络进程         │  │  GPU 进程         │                │
│  │ (HTTP/Cookie)    │  │ (合成、画图)      │                │
│  └──────────────────┘  └──────────────────┘                 │
│                                                             │
│  ┌──────────────────┐                                       │
│  │  插件进程 / 扩展  │                                       │
│  └──────────────────┘                                       │
└─────────────────────────────────────────────────────────────┘

各进程职责

进程职责
浏览器主进程,管 UI、菜单、tab、子进程调度
渲染解析 HTML/CSS、跑 JS、合成图层(每个 tab 一个)
网络处理网络请求(HTTP/HTTPS/DNS/Cookie)
GPU处理 3D / 合成 / 部分 2D 加速
插件浏览器插件、扩展
存储IndexedDB、Cache 等持久化(独立进程)

⚠️ Site Isolation:从 2018 年起,不同站点(不只是不同 tab)会被隔离到不同渲染进程,防止 Spectre 漏洞利用。

渲染进程内部

渲染进程内部又有多个线程

┌──── 渲染进程 ────────────────────────┐
│  ① GUI 渲染线程    ─ 排版、绘制         │
│  ② JS 引擎线程    ─ V8 跑 JS(与 ①    │
│                     互斥)              │
│  ③ 事件触发线程    ─ 把事件塞进任务队列  │
│  ④ 定时器线程     ─ setTimeout/Interval│
│  ⑤ 网络异步线程    ─ XHR/fetch 回调     │
│  ⑥ 合成线程       ─ 把图层合成           │
└─────────────────────────────────────┘

关键点:JS 引擎线程和 GUI 渲染线程互斥——JS 跑得久了,页面就卡。


2. 从输入 URL 到页面渲染(必背!)

这是面试必考题。完整答案要分 5 个阶段:

┌──────────────────────────────────────────────────────────┐
│         浏览器地址栏输入 https://www.example.com         │
└──────────────────────────────────────────────────────────┘

┌─── 阶段一:URL 解析 + 缓存检查 ─────────────────────────┐
│  • 检查是否非法 URL(按搜索处理)                        │
│  • 检查 HSTS 缓存(强制 HTTPS)                          │
│  • 查浏览器缓存:strong cache 命中 → 直接渲染            │
└──────────────────────────────────────────────────────────┘

┌─── 阶段二:DNS 解析 ────────────────────────────────────┐
│  浏览器 DNS 缓存                                         │
│      ↓ miss                                              │
│  操作系统 DNS 缓存(hosts → /etc/hosts)                 │
│      ↓ miss                                              │
│  本地 DNS 服务器(运营商)                               │
│      ↓ miss                                              │
│  根域名服务器 → .com 顶级 → example.com 权威            │
│      ↓                                                   │
│  得到 IP(如 93.184.216.34)                             │
└──────────────────────────────────────────────────────────┘

┌─── 阶段三:建立连接 ────────────────────────────────────┐
│  • TCP 三次握手                                          │
│      Client → SYN → Server                              │
│      Client ← SYN+ACK ← Server                          │
│      Client → ACK → Server                              │
│  • TLS 握手(HTTPS)                                     │
│      Client Hello → Server Hello → 证书验证 → 密钥协商   │
│  • HTTP/2 / HTTP/3 还涉及多路复用、QUIC                  │
└──────────────────────────────────────────────────────────┘

┌─── 阶段四:发送请求 + 接收响应 ─────────────────────────┐
│  • 发 HTTP 请求(Request Line + Headers + Body)         │
│  • 服务器处理(路由、业务、查 DB……)                     │
│  • 返回 Response(状态行 + Headers + Body)              │
│  • 浏览器收到 HTML 流(边收边解析)                      │
└──────────────────────────────────────────────────────────┘

┌─── 阶段五:浏览器渲染 ──────────────────────────────────┐
│  1. 解析 HTML → DOM 树                                   │
│  2. 解析 CSS → CSSOM 树                                  │
│  3. 合成 Render Tree(DOM + CSSOM,去掉 display:none)  │
│  4. Layout:计算每个节点的几何信息(位置、尺寸)         │
│  5. Paint:把每个节点画到对应图层                        │
│  6. Composite:GPU 合成图层 → 显示到屏幕                 │
│  7. JS 执行(解析 → 编译 → 执行;可能阻塞渲染)          │
│  8. 触发 DOMContentLoaded / load 事件                    │
└──────────────────────────────────────────────────────────┘

                    用户看到页面

              四次挥手关闭 TCP 连接

几个考点细节

TCP 三次握手为什么是三次? 两次不够:服务端无法确认客户端的接收能力;四次浪费。三次刚好双向确认。

为什么挥手要四次? TCP 是全双工的,关闭时双方各发一次 FIN+ACK。

HTTPS 握手简化版

1. ClientHello:客户端发支持的 TLS 版本、密码套件、随机数 R1
2. ServerHello:服务端选定 TLS 版本/套件,发随机数 R2 + 证书 + 公钥
3. 客户端验证证书 → 用公钥加密"预主密钥 R3"发回
4. 双方用 R1 + R2 + R3 计算出对称密钥
5. 后续通信用对称密钥加密

详见第 24 章。


3. 渲染流水线详解

HTML  ─────►  Tokens  ─────►  DOM Tree

CSS   ─────►  Tokens  ─────►  CSSOM Tree  ╲
                                            ▶  Render Tree
                                            ▶  Layout(几何)
                                            ▶  Paint(绘制指令)
                                            ▶  Composite(合成)
                                            ▶  屏幕显示

① 解析 HTML → DOM

边下边解析(流式),生成树形结构:

<html>
  <head><title>Hi</title></head>
  <body>
    <h1>Hello</h1>
  </body>
</html>



document
  └─ html
       ├─ head
       │    └─ title — "Hi"
       └─ body
            └─ h1 — "Hello"

② 解析 CSS → CSSOM

body { font-size: 16px; }
h1   { color: red; }



CSSOM
  └─ body
       └─ font-size: 16px
            └─ h1
                 └─ color: red
                 └─ font-size: 16px (继承)

③ 构建 Render Tree

DOM + CSSOM 合并,去掉不显示的节点display: none 的、<head> 等)。

⚠️ visibility: hidden 仍然在 Render Tree 里(占位置但不可见),display: none 不在。

④ Layout(布局 / 回流)

计算每个节点的精确位置和大小(盒模型 + 文档流 + 浮动 + flex/grid)。

⑤ Paint(绘制 / 重绘)

把每个节点画到一个或多个图层上(绘制指令列表,还没真画到屏幕)。

⑥ Composite(合成)

把多个图层送到 GPU,由合成线程合成最终位图,输出到屏幕。


4. 回流 vs 重绘 vs 合成

阶段触发代价
回流几何属性变化(width/height/position/display)最贵,整树重新计算
重绘仅外观变化(color/background/visibility)中等,跳过 Layout
合成仅 transform / opacity最便宜,仅 GPU 工作
变更属性

   ├── width/height/font-size/display ──► Layout → Paint → Composite【回流】

   ├── color/background/box-shadow ─────► Paint → Composite        【重绘】

   └── transform/opacity ───────────────► Composite                【合成】

强制同步布局(Layout Thrashing)

js
// ❌ 每次循环都触发回流
for (let i = 0; i < items.length; i++) {
  items[i].style.width = items[i].offsetWidth + 10 + 'px';
  // 读 offsetWidth → 强制 Layout → 写入 → 又一次 Layout
}

// ✅ 先批读,再批写
const widths = items.map(it => it.offsetWidth);
items.forEach((it, i) => it.style.width = widths[i] + 10 + 'px');

5. 合成层(Composite Layer)与 GPU 加速

什么是合成层

浏览器会把"动起来更频繁"的元素单独提为一个图层,让 GPU 单独合成,避免动一下就回流整个页面。

触发合成层的条件

条件说明
transform: translateZ(0) / translate3d(0,0,0)经典的"hack",强制 GPU
will-change: transform / opacity现代标准方法
<video> / <canvas>自带合成层
position: fixed通常会
opacity 动画期间临时层
filter / backdrop-filter触发

收益

  • 动画只在合成阶段做,不进入 Layout/Paint,60fps 丝滑。
  • 这一层重绘不影响其他层。

代价

  • 吃显存:每个层的位图都要存到 GPU 内存。
  • 滥用 will-change 会让首屏更慢(创建层有开销)。

⚠️ 经典反模式:给上千个元素都加 will-change: transform,浏览器卡到崩。用了就要在动画结束后 will-change: auto


6. 事件循环(浏览器版)

JavaScript 是单线程的,靠"事件循环"实现异步。

主循环

┌────────────────────────────────────────┐
│            事件循环(一圈)              │
├────────────────────────────────────────┤
│  1. 取一个宏任务执行                    │
│  2. 执行完后:清空整个微任务队列        │
│  3. requestAnimationFrame 回调         │
│  4. 渲染(如果有变化)                  │
│  5. requestIdleCallback(空闲时)      │
│  6. 回到 1                             │
└────────────────────────────────────────┘

宏任务 vs 微任务

类型例子执行时机
宏任务setTimeoutsetIntervalMessageChannel、I/O、UI 事件每轮取一个
微任务Promise.thenMutationObserverqueueMicrotask每个宏任务后全清空

经典面试题

js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

// 输出:1 → 4 → 3 → 2
// 同步代码 → 微任务 → 下一轮宏任务

requestAnimationFrame vs requestIdleCallback

API何时执行用途
requestAnimationFrame下一帧渲染前流畅动画、读 DOM 测量
requestIdleCallback浏览器空闲时(一帧渲染完后剩余时间)非紧急任务(埋点、预取)
js
// 60fps 动画
function tick() {
  element.style.transform = `translateX(${x}px)`;
  x += 1;
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

// 空闲时上报埋点
requestIdleCallback(deadline => {
  while (deadline.timeRemaining() > 0 && queue.length) {
    sendOne(queue.shift());
  }
});

7. 跨标签页通信

方式同源限制持久性适用场景
localStorage 事件同源持久简单跨 tab 通知
BroadcastChannel同源临时现代首选,多 tab 群聊
postMessage跨源 ✅临时iframe / 弹出窗口通信
SharedWorker同源临时多 tab 共享一份后台任务/状态
Service Worker同源持久离线、推送、消息中转

1. localStorage 事件(最古老)

js
// Tab A
localStorage.setItem('msg', 'hello' + Date.now());

// Tab B
window.addEventListener('storage', e => {
  if (e.key === 'msg') console.log('收到:', e.newValue);
});
// 注意:只在"其他 tab"触发,发送方自己不会收到

2. BroadcastChannel(推荐 ✅)

js
// 任意 tab
const ch = new BroadcastChannel('app');
ch.postMessage({ type: 'login', user: 'alice' });
ch.onmessage = e => console.log('收到:', e.data);

3. postMessage(跨源)

js
// 父页面 → iframe
iframe.contentWindow.postMessage({ hi: 1 }, 'https://other.com');

// iframe 接收
window.addEventListener('message', e => {
  if (e.origin !== 'https://parent.com') return; // ⚠️ 一定要校验来源!
  console.log(e.data);
});

4. SharedWorker

js
// shared.js
const ports = [];
onconnect = e => {
  const port = e.ports[0];
  ports.push(port);
  port.onmessage = ev => ports.forEach(p => p.postMessage(ev.data));
};

// 各 tab
const w = new SharedWorker('shared.js');
w.port.start();
w.port.postMessage('hi');
w.port.onmessage = e => console.log(e.data);

详见 examples/tab-communication.html


8. ⚠️ 易踩的坑

  1. 以为 <script> 不阻塞 HTML 解析——默认是阻塞的!加 defer(DOM 解析完后按序执行)或 async(下载完立即执行,不保证顺序)。

  2. DOMContentLoadedload 区别

    • DOMContentLoaded:DOM 树构建完成(不等图片、样式表完成)
    • load:所有资源(图、CSS、iframe)都加载完
    • 一般注册事件用前者更快。
  3. requestAnimationFrame 在 tab 后台时会暂停——切换标签时动画会"冻住"。这其实是省电特性。

  4. postMessage 不校验 e.origin——存在 XSS 入口。永远校验来源。

  5. 强制同步布局陷阱——读 offsetWidth 后立刻写,再读再写……FPS 直接掉到 5。

  6. 以为微任务会"暂停"宏任务——错。微任务是"清空",如果一个微任务里再 push 微任务,会一直清下去,可能导致页面卡死。

  7. 跨 tab 通信用 localStorage 同 tab 触发——同 tab 不触发 storage 事件,只在其他 tab 触发。

  8. GPU 层数过多:DevTools → Layers 面板能看到层。每层都要显存,几百层就崩。

  9. JS 长任务把所有事件都积压:单次 JS > 50ms 就会让页面无响应(INP 飙高)。要拆任务、用 Worker。

  10. 以为 setTimeout(fn, 0) 是真的 0ms——HTML5 规范最小 4ms,且要等整个事件循环排到。


9. 一句话总结

浏览器是「多进程的画家」:网络进程拿原料,渲染进程把 HTML/CSS/JS 解析成 DOM/CSSOM,组成 Render Tree,经过 Layout → Paint → Composite 三步在 GPU 上画到屏幕;中间所有事件、定时器、Promise,都靠事件循环调度——理解了这个流程,你就能解释一切前端性能、渲染、异步问题。


10. 延伸阅读