> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 浏览器渲染

> 覆盖 URL 到页面渲染全链路：导航、DNS、TLS、HTTP 版本、DOM/CSSOM 与布局合成。

> 我对浏览器渲染的理解分了几次补：起初只记得从 URL 到页面的大致流程，后来才弄清每个阶段各自在解决什么问题，最后能按需要拆到某一步的细节。

***

## Q1: 从输入 URL 到页面展示，经历了哪些步骤？

**1. URL 解析与导航开始**

用户在地址栏输入内容后，浏览器首先判断是搜索关键词还是 URL。如果包含 `.` 且没有空格，浏览器会补全协议（默认 `https://`）并开始导航。浏览器主进程通过 IPC 通知网络进程发起请求。

**2. DNS 解析**

浏览器先查自身 DNS 缓存，再查操作系统缓存和 hosts 文件，最后由本地 DNS 服务器执行迭代查询——根 DNS 返回 `.com` 服务器地址，`.com` DNS 返回权威服务器地址，权威 DNS 返回最终的 A 记录。

**3. TCP 三次握手与 TLS 握手**

客户端与服务器通过三次握手建立 TCP 连接（SYN → SYN+ACK → ACK）。HTTPS 场景下还需要 TLS 握手——TLS 1.3 优化为 1-RTT，支持 0-RTT 恢复。QUIC（HTTP/3）将传输层握手和 TLS 握手合并，首次连接只需 1-RTT。

**4. HTTP 请求与服务器响应**

浏览器发送 HTTP 请求报文（请求行 + 请求头 + 请求体），服务器返回响应（状态码 + 响应头 + 响应体）。HTTP/1.1 有队头阻塞，HTTP/2 多路复用解决 HTTP 层阻塞，HTTP/3 用 QUIC 解决 TCP 层阻塞。

**5. 浏览器解析与渲染**

渲染进程将 HTML 解析为 DOM 树，CSS 解析为 CSSOM 树，两者合并为渲染树。然后经过 Layout（几何计算）、Paint（绘制指令）、Composite（GPU 合成）三个阶段，最终输出到屏幕。

**浏览器渲染解决的核心问题：**

```text theme={null}
问题：用户输入一个 URL，浏览器如何把一堆字节变成屏幕上可交互的页面？

收到 HTML 字节流 → ？？？ → 屏幕像素

解决：渲染流程（Rendering Pipeline）

  HTML 字节流 → Token → DOM 树
  CSS 字节流 → Token → CSSOM 树
  DOM + CSSOM → 渲染树（只含可见节点）
  渲染树 → Layout（几何计算）→ Paint（绘制指令）→ Composite（GPU 合成）
```

> [!NOTE]
>
> 浏览器渲染的本质是把 HTML/CSS 字节流经过解析、布局、绘制、合成，最终变成屏幕上的像素。

> [card] 没有渲染流程
>
> 如果浏览器收到 HTML 后直接尝试绘制像素，就像一个建筑工人拿到一摞建材清单后直接砌墙——没有图纸（DOM 树）、没有结构设计（Layout），最终只能得到一堆乱七八糟的砖块。

> [card] 引入渲染流程
>
> 有了渲染流程后，就像建筑工地有了完整的流程：先读清单建骨架（DOM），再画装修图纸（CSSOM），然后出施工图（Layout），最后按图施工（Paint）并交付（Composite）。每一步职责清晰，层层递进。

> [!TIP]
>
> 回答时先给宏观框架（30 秒说出 7 个阶段），然后等对方追问某一步的细节。这展示了你的知识体系和沟通能力。不要一上来就背诵 DNS 的迭代查询细节。

***

## Q2: URL 解析与导航开始

用户在地址栏输入内容后，浏览器做了什么？

```text theme={null}
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│ 地址栏输入   │ →   │ URL 解析     │ →   │ 导航开始     │
│              │     │ 补全协议     │     │ 通知网络进程 │
└──────────────┘     └──────────────┘     └──────────────┘
```

浏览器首先判断输入的是搜索关键词还是 URL。如果包含 `.` 且没有空格，浏览器会补全协议（默认 `https://`）并开始导航。浏览器主进程（Browser Process）通过 IPC 通知网络进程（Network Process）发起请求，同时会触发 `beforeunload` 事件给当前页面，如果当前页面有未保存的表单数据，可以在此阶段拦截导航。

导航开始时，浏览器还会检查 HSTS（HTTP Strict Transport Security）列表，如果目标域名在列表中，会强制将 `http://` 升级为 `https://`，避免中间人攻击。

> [!WARNING]
>
> HSTS 是服务器通过响应头 `Strict-Transport-Security` 告诉浏览器："以后访问我必须用 HTTPS"。浏览器会把这个域名加入 HSTS 列表，下次访问时自动升级为 HTTPS，即使用户手动输入 `http://`。这可以防止 SSL 剥离攻击（中间人把 HTTPS 降级为 HTTP）。

***

## Q3: DNS 解析：域名到 IP 的转换

浏览器怎么把 `www.example.com` 变成 `93.184.216.34`？

```text theme={null}
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ 浏览器   │ →  │ 本地DNS  │ →  │ 根DNS    │ →  │ 权威DNS  │
│ 缓存     │    │ 服务器   │    │ (.com)   │    │(example) │
└──────────┘    └──────────┘    └──────────┘    └──────────┘
   先查自己       再查 114        查黄页目录      找到电话号码
```

DNS 解析分两步：**递归查询**（浏览器到本地 DNS 服务器）和**迭代查询**（本地 DNS 到根/顶级域/权威 DNS）。

浏览器先查自身 DNS 缓存（`chrome://net-internals/#dns` 可查看），缓存 TTL 通常与 DNS 记录一致；再查操作系统缓存（`getaddrinfo` 系统调用）和 hosts 文件（`/etc/hosts`）；最后由本地 DNS 服务器（通常由 ISP 或 `8.8.8.8` 等公共 DNS 提供）执行迭代查询。

> [!WARNING]
>
> DNS 默认用 UDP（端口 53），响应超过 512 字节时切换 TCP。DNSSEC 签名响应通常超过 512 字节，所以 DNSSEC 环境下几乎总是走 TCP。Chrome 还支持 DNS-over-HTTPS（DoH），将 DNS 查询封装在 HTTPS 请求中，防止 DNS 劫持。

### DNS 缓存机制

DNS 解析结果会被缓存在多个层级：

| 层级 | 存储位置 | TTL |
| - | - | - |
| 浏览器缓存 | `chrome://net-internals/#dns` | 与 DNS 记录一致 |
| 操作系统缓存 | `getaddrinfo` 系统调用 | 与 DNS 记录一致 |
| hosts 文件 | `/etc/hosts` | 永久（手动修改） |
| 本地 DNS 服务器 | ISP 或公共 DNS | 与 DNS 记录一致 |

浏览器查询 DNS 时，先查浏览器缓存，再查操作系统缓存和 hosts 文件，最后才问本地 DNS 服务器。如果本地 DNS 服务器有缓存且未过期，直接返回，不需要迭代查询。

### DNS 劫持和 DNS-over-HTTPS

DNS 劫持是指攻击者篡改 DNS 解析结果，把用户引导到恶意网站。常见手段：运营商 DNS 劫持、中间人攻击、路由器劫持。

DNS-over-HTTPS（DoH）将 DNS 查询封装在 HTTPS 请求中，加密传输，防止中间人篡改。Chrome 从 2020 年开始默认启用 DoH（`chrome://settings/security` → 使用安全 DNS）。

> [!TIP]
>
> DNS 解析就像查电话簿：先查自己的手机通讯录（浏览器缓存），没有就打 114（本地 DNS），114 再帮你查黄页（根 DNS），最后找到具体的电话号码（权威 DNS）。如果 114 也不知道，它会一层层往上问，直到找到答案。

***

## Q4: TCP 三次握手与 TLS 握手

客户端和服务器怎么建立连接？

```text theme={null}
客户端                          服务器
  │                               │
  │──── SYN (Seq=x) ────────────→│  1. 客户端：我要连接
  │                               │
  │←─── SYN+ACK (Seq=y,Ack=x+1) ─│  2. 服务器：好的，我也准备好了
  │                               │
  │──── ACK (Ack=y+1) ──────────→│  3. 客户端：确认，连接建立
  │                               │
```

TCP 三次握手的目的是**同步初始序列号并确认收发能力**。客户端发送 SYN（Seq=x），服务器回复 SYN+ACK（Seq=y, Ack=x+1），客户端发送 ACK（Ack=y+1），双方确认能互相收发数据。

HTTPS 场景下还需要 TLS 握手。TLS 1.2 需要 2-RTT（先协商加密套件和密钥，再交换证书验证身份），TLS 1.3 优化为 1-RTT（将密钥交换和证书验证合并到同一批消息），还支持 0-RTT 恢复（通过 Pre-Shared Key 在第一个数据包中就携带应用数据，但有重放攻击风险）。

QUIC（HTTP/3）将传输层握手和 TLS 1.3 握手合并为一个过程，首次连接只需 1-RTT，恢复连接只需 0-RTT，彻底消除了"先建 TCP 连接再建 TLS 连接"的两步延迟。

> [!TIP]
>
> TCP 三次握手就像打电话确认对方能听到：你拨号说"喂，能听到吗？"（SYN），对方接听说"能听到，你呢？"（SYN+ACK），你说"我也能听到"（ACK）——双方都确认能听到对方，通话建立。TLS 握手就像在通话前先约定暗号——双方确认用什么加密方式，验证对方身份。

### 为什么不能两次握手？

两次握手的问题是：服务器回复 SYN+ACK 时，客户端还没确认自己能收到服务器的数据。如果只握手两次，服务器不知道客户端能不能收到自己的回复，可能导致服务器单方面建立连接——客户端根本没收到服务器的响应，但服务器以为连接已建立，白白浪费资源。

三次握手的第三步（ACK）让服务器确认客户端能收到自己的回复，双方都确认收发能力后才建立连接。

### TCP 四次挥手的过程

```text theme={null}
客户端                          服务器
  │                               │
  │──── FIN ─────────────────────→│  1. 客户端：我发完了
  │                               │
  │←─── ACK ──────────────────────│  2. 服务器：知道了（但我可能还有数据没发完）
  │                               │
  │←─── FIN ──────────────────────│  3. 服务器：我也发完了
  │                               │
  │──── ACK ─────────────────────→│  4. 客户端：确认，连接关闭
  │                               │
```

为什么是四次而不是三次？因为 TCP 是全双工的——关闭发送通道和接收通道是独立的操作。服务器收到客户端的 FIN 时，可能还有数据没发完，所以先回 ACK 确认收到，等数据发完再发 FIN。

### TLS 1.2 和 1.3 的区别

| 对比维度 | TLS 1.2 | TLS 1.3 |
| - | - | - |
| 握手 RTT | 2-RTT | 1-RTT |
| 密钥交换 | RSA / DH / ECDHE | 只有 ECDHE（前向安全） |
| 加密套件 | 支持 CBC、SHA-1 等不安全算法 | 只保留 AEAD 加密套件 |
| 0-RTT 恢复 | 不支持 | 支持（有重放攻击风险） |
| 证书验证 | 握手过程中交换 | 合并到前两条消息 |

TLS 1.3 的核心优化是**将密钥交换和证书验证合并到同一批消息**——客户端在 ClientHello 时就附带 DH 密钥共享，服务器在 ServerHello 时就完成密钥交换。这减少了 1 个 RTT。

### 0-RTT 恢复的风险

**什么是 RTT？** RTT（Round-Trip Time）是数据从客户端到服务器再回来的时间。一个 RTT 大约 50-200ms（取决于距离）。TLS 握手需要 1-2 个 RTT，意味着每次 HTTPS 连接要多等 100-400ms。

**什么是 0-RTT？** 0-RTT 是 TLS 1.3 的特性——客户端在第一个数据包中就携带应用数据，不需要等服务器回复。就像你打电话时，不等对方接就直接说"帮我订一杯咖啡"，对方接起电话就听到了，省了一个来回的时间。

**风险是什么？** 攻击者可以截获你的 0-RTT 数据包，复制一份发给服务器。服务器无法区分是新请求还是重放的旧请求——如果你发的是"转 100 元"，攻击者重放一次就多转了 100 元。

**所以：** 0-RTT 只适合幂等操作（GET 请求，重复执行结果一样），不适合敏感操作（POST、支付、转账）。

***

## Q5: HTTP 请求与服务器响应

浏览器和服务器之间怎么交换数据？

```text theme={null}
HTTP/1.1: 一次一个请求，排队等待
  请求1 → 响应1 → 请求2 → 响应2 → ...（队头阻塞）

HTTP/2: 多路复用，一个连接并行传输
  请求1 ──→ 响应1
  请求2 ──→ 响应2    （同一 TCP 连接，Stream ID 交错）
  请求3 ──→ 响应3

HTTP/3: 每个流独立处理丢包（QUIC/UDP）
  流1 ──→ 响应1（丢包只影响流1）
  流2 ──→ 响应2（不受影响）
  流3 ──→ 响应3（不受影响）
```

浏览器发送 HTTP 请求报文，包含请求行（方法 + 路径 + 协议版本）、请求头（Cookie、Accept、Accept-Encoding、User-Agent 等）和可选的请求体；服务器处理后返回响应（状态码 + 响应头 + 响应体），响应头中的 `Content-Type` 决定浏览器如何解析响应体（`text/html` 走渲染流程，`application/json` 走 JS 处理）。HTTP/1.1 曾提出管线化（pipelining），但响应必须按请求顺序返回，且主流浏览器默认关闭，实际并未解决队头阻塞。

> [!WARNING]
>
> HTTP/2 的多路复用解决了 HTTP 层的队头阻塞，但 TCP 层的队头阻塞仍在——一个 TCP 包丢失会阻塞该连接上所有流。HTTP/3 用 QUIC（基于 UDP）为每个 Stream 独立处理丢包，彻底解决了这个问题。

> [!TIP]
>
> HTTP/1.1 是一次只能发一个快递，发完才能发下一个（队头阻塞）。HTTP/2 是一辆卡车装多个快递，但一个坏了全车等（TCP 层阻塞）。HTTP/3 是每个快递独立运输，一个坏了不影响其他（QUIC/UDP）。

### HTTP 缓存策略：强缓存 vs 协商缓存

浏览器缓存分为**强缓存**（不发请求，直接用缓存）和**协商缓存**（发请求问服务器，服务器说用缓存就用缓存）。

```text theme={null}
┌──────────────────────────────────────────────┐
│ 请求 → 强缓存命中？ → 是 → 直接用缓存（200 from cache）│
│         │                                      │
│         否                                     │
│         ▼                                      │
│     协商缓存命中？ → 是 → 304 Not Modified      │
│         │                                      │
│         否                                     │
│         ▼                                      │
│     服务器返回新资源（200 OK）                   │
└──────────────────────────────────────────────┘
```

| 类型 | 头部字段 | 行为 |
| - | - | - |
| 强缓存 | `Cache-Control: max-age=3600` | 3600 秒内直接用缓存，不发请求 |
| 强缓存 | `Expires: Thu, 01 Jan 2025 00:00:00 GMT` | 到期时间前直接用缓存（已废弃） |
| 协商缓存 | `ETag` / `If-None-Match` | 服务器返回资源的哈希值，下次请求带上，服务器对比决定是否用缓存 |
| 协商缓存 | `Last-Modified` / `If-Modified-Since` | 服务器返回资源的最后修改时间，下次请求带上，服务器对比决定是否用缓存 |

**ETag vs Last-Modified**：ETag 基于内容哈希，精确到字节级别；Last-Modified 基于修改时间，精度只到秒。ETag 优先级更高——如果同时存在，浏览器优先用 ETag。

### HTTP 状态码详解

| 状态码 | 含义 | 常见场景 |
| - | - | - |
| **200** | 成功 | 正常请求 |
| **301** | 永久重定向 | HTTP → HTTPS |
| **302** | 临时重定向 | 登录后跳转 |
| **304** | 未修改（协商缓存命中） | 浏览器用缓存 |
| **400** | 请求错误 | 参数格式错误 |
| **401** | 未认证 | 未登录 |
| **403** | 无权限 | 已登录但没权限 |
| **404** | 未找到 | URL 不存在 |
| **500** | 服务器内部错误 | 服务端代码异常 |
| **502** | 网关错误 | 上游服务器无响应 |
| **503** | 服务不可用 | 服务器过载 |

### 跨域解决方案（CORS）

浏览器的同源策略限制了跨域请求。CORS（Cross-Origin Resource Sharing）是 W3C 标准的跨域解决方案：

```text theme={null}
简单请求：直接发送，浏览器自动加 Origin 头
  GET/POST/HEAD + 简单头部 → 服务器返回 Access-Control-Allow-Origin

预检请求：先发 OPTIONS 请求确认服务器是否允许
  非简单请求 → OPTIONS 预检 → 服务器返回允许的头部和方法 → 实际请求
```

| 方案 | 适用场景 | 原理 |
| - | - | - |
| CORS | 生产环境首选 | 服务器返回 `Access-Control-Allow-Origin` 头 |
| 代理 | 开发环境 | 开发服务器代理请求，绕过浏览器同源策略 |
| JSONP | 已废弃 | 利用 `<script>` 标签不受同源策略限制 |

> [!WARNING]
>
> `fetch` 请求默认不携带 Cookie。如果需要携带 Cookie，必须设置 `credentials: 'include'`，且服务器必须返回 `Access-Control-Allow-Credentials: true`。此时 `Access-Control-Allow-Origin` 不能是 `*`，必须是具体的域名。

***

## Q6: 浏览器解析：从字节流到渲染树

浏览器拿到 HTML 字节后，怎么变成可以渲染的树结构？

```text theme={null}
HTML 字节流 → Token → DOM 树 ─┐
                               ├→ 渲染树（只含可见节点）
CSS 字节流 → Token → CSSOM 树 ─┘
```

HTML 解析器采用增量解析（边下载边解析），将字节流先 Tokenize 为 Token（开始标签、结束标签、文本节点），再由 TreeBuilder 构建 DOM 树。CSS 解析器独立构建 CSSOM 树，两者合并为渲染树（Render Tree）。

渲染树排除 `display:none` 的节点（不在渲染树中），但保留 `visibility:hidden` 的节点（在渲染树中但不绘制）。伪元素（`::before`/`::after`）在渲染树中生成匿名节点。

> [!WARNING]
>
> CSS 文件加载完成前，CSSOM 没就绪，渲染树无法构建，页面不会渲染。但 CSS 不阻塞 DOM 解析——HTML 解析器可以继续构建 DOM 树，只是不能渲染。JS 会阻塞 DOM 解析——浏览器遇到 `<script>` 标签时会暂停 HTML 解析，等待 JS 下载并执行完毕。

> [!TIP]
>
> DOM 树就像建筑的骨架图纸（结构），CSSOM 树就像装修图纸（样式）。两张图纸合并后才是完整的施工图（渲染树）。如果装修图纸还没画好（CSS 没加载完），施工队（渲染引擎）就无法开工——但可以先搭骨架（DOM 解析继续）。

### CSS 阻塞渲染的优化策略

CSS 阻塞渲染是因为浏览器需要 CSSOM 才能构建渲染树。优化策略：

* **内联关键 CSS**：把首屏需要的 CSS 内联到 `<style>` 标签中（控制在 14KB 以内，匹配 TCP 初始拥塞窗口）
* **异步加载非关键 CSS**：用 `<link rel="preload" as="style" onload>` 异步加载非首屏 CSS
* **CSS 放 `<head>`**：尽早开始下载，不要放在 `<body>` 底部

```html theme={null}
<!-- 内联关键 CSS -->
<style>
  /* 首屏需要的关键样式，控制在 14KB 以内 */
  .header { ... }
  .hero { ... }
</style>

<!-- 异步加载非关键 CSS -->
<link
  rel="preload"
  href="non-critical.css"
  as="style"
  onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="non-critical.css" /></noscript>
```

### JS 阻塞 DOM 解析的优化策略

JS 阻塞 DOM 解析是因为 JS 可能调用 `document.write()` 重写文档，或者访问已解析的 DOM 节点。优化策略：

* **`defer`**：不阻塞 DOM 解析，DOM 解析完后按文档顺序执行（推荐）
* **`async`**：不阻塞 DOM 解析，下载完立即执行（适合独立脚本）
* **`type="module"`**：默认 defer 行为，按依赖图顺序执行

```html theme={null}
<!-- 阻塞 DOM 解析 -->
<script src="app.js"></script>

<!-- 不阻塞 DOM 解析，DOM 解析完后按顺序执行 -->
<script defer src="app.js"></script>

<!-- 不阻塞 DOM 解析，下载完立即执行 -->
<script async src="analytics.js"></script>

<!-- ES Module，默认 defer 行为 -->
<script type="module" src="app.js"></script>
```

### 渲染树和 DOM 树的区别

| 对比维度 | DOM 树 | 渲染树 |
| - | - | - |
| 包含节点 | 所有节点（包括 `display:none`） | 只有可见节点 |
| 样式信息 | 无 | 有（计算后的完整样式） |
| 伪元素 | 不包含 | 包含（`::before`/`::after`） |
| `visibility:hidden` | 包含 | 包含（占位但不绘制） |

渲染树是 DOM 树和 CSSOM 树的合并结果。`display:none` 的节点不在渲染树中（不占位），`visibility:hidden` 的节点在渲染树中（占位但不绘制）。

***

## Q7: 渲染流程：Layout → Paint → Composite

渲染树构建完成后，怎么变成屏幕上的像素？

```text theme={null}
渲染树 → Layout（几何计算）→ Paint（绘制指令）→ Composite（GPU 合成）→ 屏幕
```

**Layout（布局/重排）**：计算每个节点的精确几何信息（位置、大小、边距）。盒模型计算在此阶段完成——content + padding + border + margin。Flexbox/Grid 布局的子元素分配也在此阶段。输出是 Layout Tree（带几何信息的渲染树）。

**Paint（绘制）**：将渲染树转化为绘制指令列表（Paint Records）。按 Stacking Context 规则生成绘制指令——背景、边框、浮动、行内元素、定位元素，从低到高依次绘制。

**Composite（合成）**：由 GPU 合成器将多个图层（Composited Layers）合并为最终画面。`transform` 和 `opacity` 动画在此阶段执行，不需要重排重绘，性能最好。

> [!WARNING]
>
> 重排（Reflow）需要重新计算布局，代价最高——影响当前节点及其所有子节点。重绘（Repaint）只需要重新生成绘制指令，代价较低。合成（Composite）只需要 GPU 矩阵变换，代价最低。优化策略：尽量用 `transform`/`opacity` 做动画，避免触发布局属性变化。

> [!TIP]
>
> Layout 就像施工队量房间尺寸、算家具位置（几何计算）。Paint 就像按图纸刷墙、铺地板（绘制指令）。Composite 就像最后的软装——把家具摆好、挂上窗帘（GPU 合成）。如果只是换个窗帘颜色（opacity 变化），不需要重新量房间（重排），直接换就行。

### 重排（Reflow）的触发条件

重排需要重新计算节点的几何信息，代价最高。触发条件：

* DOM 增删（插入/删除节点）
* 元素尺寸变化（`width`、`height`、`padding`、`margin`、`border`）
* 内容变化（文本增减、图片尺寸变化）
* 浏览器窗口 resize
* 读取布局属性（`offsetWidth`、`offsetHeight`、`getComputedStyle`、`getBoundingClientRect`）

> [!WARNING]
>
> 读取布局属性（如 `offsetWidth`）后立即修改样式，会触发强制同步布局——浏览器必须先完成之前的样式修改，才能返回准确的布局信息。这会导致一帧内多次重排，性能极差。
>
> ```javascript theme={null}
> // ❌ 强制同步布局：读取 → 修改 → 读取 → 修改（多次重排）
> for (const el of elements) {
>   el.style.width = el.offsetWidth + 10 + "px"; // 每次循环都重排
> }
>
> // ✅ 批量读取 → 批量修改（一次重排）
> const widths = elements.map((el) => el.offsetWidth);
> elements.forEach((el, i) => (el.style.width = widths[i] + 10 + "px"));
> ```

### 重绘（Repaint）的触发条件

重绘只需要重新生成绘制指令，不影响布局，代价较低。触发条件：

* 颜色变化（`color`、`background-color`、`border-color`）
* 阴影变化（`box-shadow`、`text-shadow`）
* 可见性变化（`visibility`、`outline`）
* 文本装饰变化（`text-decoration`）

### Composite 阶段的优化

合成阶段由 GPU 执行，只需要矩阵变换，不需要重排重绘。触发合成的操作：

* `transform`（位移、缩放、旋转）
* `opacity`（透明度）
* `will-change`（提前告知浏览器哪些属性会变化）

```javascript theme={null}
// 只触发 Composite，不触发 Layout 和 Paint
element.style.transform = "translateX(100px)";
element.style.opacity = 0.5;

// 触发 Layout + Paint + Composite，性能差
element.style.left = "100px";
element.style.width = "200px";
```

优化策略：用 `transform` 做位移/缩放/旋转动画，用 `opacity` 做透明度动画，避免触发布局属性（`width`、`height`、`margin`、`padding`）变化。

***

## Q8: script 标签的加载方式有什么区别？

```text theme={null}
普通 <script>：阻塞 DOM 解析，下载完立即执行
<script async>：不阻塞 DOM 解析，下载完立即执行（不保证顺序）
<script defer>：不阻塞 DOM 解析，DOM 解析完后按顺序执行
<script type="module">：默认 defer 行为
```

| 方式 | 阻塞 DOM 解析 | 执行时机 | 适用场景 |
| - | - | - | - |
| `<script>` | ✅ 阻塞 | 下载完立即执行 | 需要立即执行且依赖 DOM |
| `<script async>` | ❌ 不阻塞 | 下载完立即执行 | 独立脚本（如统计 SDK） |
| `<script defer>` | ❌ 不阻塞 | DOM 解析完后按顺序 | 大多数业务脚本 |
| `<script type="module">` | ❌ 不阻塞 | 按依赖图顺序 | ES Modules |

> [!WARNING]
>
> `async` 脚本下载完就执行，不保证顺序——如果页面有两个 async 脚本，谁先下载完谁先执行。`defer` 脚本在 DOM 解析完成后按文档顺序执行——保证顺序。大多数业务脚本应该用 `defer`，独立脚本（如统计 SDK）可以用 `async`。

***

## 底层源码：Chromium 是如何实现的？

Blink 的渲染流水线是怎么实现的？

Blink 的渲染流水线在 `LocalFrame::ScheduleVisualUpdate()` 中触发一帧渲染（详见 [Blink 源码 local\_frame.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/frame/local_frame.cc)）。每一帧的执行顺序是：

1. **Style Calculation**：计算每个节点的最终样式（`StyleEngine::RecalcStyle()`）
2. **Layout**：计算几何信息（`LayoutView::Layout()`）
3. **Paint**：生成绘制指令（`PaintController::Paint()`）
4. **Composite**：GPU 合成（`LayerTreeHost::Composite()`）

```cpp theme={null}
// LocalFrame::ScheduleVisualUpdate() 触发一帧渲染
void LocalFrame::ScheduleVisualUpdate() {
  // 通知合成器需要更新
  if (GetDocument().IsActive()) {
    GetPage()->GetChromeClient().ScheduleAnimationUpdate();
  }
}

// 每一帧的执行顺序（简化）
void LocalFrame::BeginMainFrame() {
  // 1. Style Calculation
  GetDocument().UpdateStyle();
  // 2. Layout
  GetDocument().UpdateLayout();
  // 3. Paint
  GetDocument().UpdatePaint();
  // 4. Composite（由合成线程执行）
}
```

渲染树和 DOM 树有什么区别？

DOM 树包含所有节点（包括 `display:none` 的节点），渲染树只包含可见节点。渲染树是 DOM 树和 CSSOM 树的合并结果，每个节点都有计算后的完整样式信息。详见 [Blink 源码 style\_engine.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/css/style_engine.cc)。

***

## 及格回答 vs 满分回答

### 问题："从输入 URL 到页面展示，发生了什么？"

**及格回答（60-70分）：**

> "浏览器先做 DNS 解析，然后建立 TCP 连接，发送 HTTP 请求，服务器返回 HTML，浏览器解析 HTML 构建 DOM 树，再解析 CSS 构建 CSSOM 树，两者合并为渲染树，然后 Layout、Paint、Composite，最终显示在屏幕上。"

问题出在：**背了流程但不理解每一步的细节**。

**满分回答（90-100分）：**

> 从输入 URL 到页面展示，浏览器要经历 7 个阶段。首先是网络阶段：URL 解析（补全协议、检查 HSTS）、DNS 解析（浏览器缓存 → 操作系统缓存 → 本地 DNS 迭代查询）、TCP 三次握手（SYN → SYN+ACK → ACK）、TLS 握手（TLS 1.3 优化为 1-RTT，支持 0-RTT 恢复）、HTTP 请求/响应（HTTP/2 多路复用解决 HTTP 层队头阻塞，HTTP/3 用 QUIC 解决 TCP 层队头阻塞）。Tali Garsiel 和 Paul Irish 在经典文章《How Browsers Work》中首次系统性地将浏览器内部分为解析、渲染、合成三大模块，为后续所有浏览器原理文章奠定了框架基础。<sup>\[1]</sup>

> 然后是渲染阶段：HTML 解析器采用增量解析（边下载边解析），将字节流 Tokenize 为 Token，再由 TreeBuilder 构建 DOM 树；CSS 解析器独立构建 CSSOM 树，两者合并为渲染树（排除 `display:none` 的节点）。Layout 阶段计算每个节点的精确几何信息（盒模型、Flexbox/Grid 子元素分配），Paint 阶段按 Stacking Context 规则生成绘制指令列表，Composite 阶段由 GPU 合成器将多个图层合并为最终画面。李兵在极客时间《浏览器工作原理与实践》中将渲染流水线拆解为 Style Calculation → Layout → Paint → Composite 四个阶段，并指出 Chrome 为每帧分配约 16.6ms，其中 JS 执行约 5ms、渲染约 11ms，这个时间分配直接影响动画流畅度。<sup>\[2]</sup>

> 渲染阶段有两个关键的性能陷阱：一是 CSS 阻塞渲染但不阻塞 DOM 解析——CSS 文件加载完成前，渲染树无法构建，页面不会渲染，但 HTML 解析器可以继续构建 DOM 树。二是 JS 阻塞 DOM 解析——浏览器遇到 `<script>` 标签时会暂停 HTML 解析，等待 JS 下载并执行完毕。优化策略：CSS 放 `<head>` 尽早加载，JS 用 `defer` 避免阻塞 DOM 解析，`transform`/`opacity` 动画走合成器不走主线程。<sup>\[3]</sup>

### 常见追问

> [accordion] CSS 会阻塞 DOM 解析吗？JS 会阻塞渲染吗？
>
> **答题要点：** CSS 不阻塞 DOM 解析但阻塞渲染，JS 阻塞 DOM 解析。
>
> **高分回答：** "CSS 不阻塞 DOM 解析——HTML 解析器可以继续构建 DOM 树，只是不能渲染（因为 CSSOM 没就绪，渲染树无法构建）。JS 阻塞 DOM 解析——浏览器遇到 `<script>` 标签时会暂停 HTML 解析，等待 JS 下载并执行完毕。
>
> ```html theme={null}
> <!-- CSS 不阻塞 DOM 解析，但阻塞渲染 -->
> <link rel="stylesheet" href="heavy.css" />
> <!-- HTML 解析继续，但不渲染 -->
>
> <!-- JS 阻塞 DOM 解析 -->
> <script src="app.js"></script>
> <!-- HTML 解析暂停，等待 JS 执行完 -->
>
> <!-- defer 不阻塞 DOM 解析，DOM 解析完后按顺序执行 -->
> <script defer src="app.js"></script>
> ```
>
> 这是因为 JS 可能调用 `document.write()` 重写文档，或者通过 `document.querySelector` 访问已解析的 DOM 节点，浏览器无法预知脚本行为，只能保守地暂停。"

> [accordion] 什么是关键渲染路径（CRP）？怎么优化？
>
> **答题要点：** CRP 是从收到 HTML 到首次渲染的路径，优化关键是减少关键资源数量和缩短关键路径长度。
>
> **高分回答：** "关键渲染路径（Critical Rendering Path）是从收到 HTML 到首次渲染的路径：HTML → DOM → CSSOM → 渲染树 → Layout → Paint。优化策略：1) 内联关键 CSS（控制在 14KB 以内，匹配 TCP 初始拥塞窗口）；2) 延迟非关键 CSS 用 `<link rel='preload' as='style' onload>` 异步加载；3) JS 用 `defer` 避免阻塞 DOM 解析；4) SSR/SSG 直出 HTML 让浏览器立即开始解析。"

> [accordion] 重排和重绘有什么区别？怎么减少重排？
>
> **答题要点：** 重排是重新计算布局，重绘是重新生成绘制指令。重排比重绘代价高得多。
>
> **高分回答：** "重排（Reflow）需要重新计算节点的几何信息（位置、大小），影响当前节点及其所有子节点，代价最高。重绘（Repaint）只需要重新生成绘制指令，不影响布局，代价较低。减少重排的方法：1) 批量修改样式（用 class 一次性改）；2) 避免读取布局属性（`offsetWidth`、`getComputedStyle`）后立即修改样式（会触发强制同步布局）；3) 用 `transform`/`opacity` 做动画，走合成器不走主线程。"

> [accordion] HTTP/2 一定比 HTTP/1.1 快吗？
>
> **答题要点：** 不一定。HTTP/2 解决了 HTTP 层的队头阻塞，但 TCP 层的队头阻塞仍在。
>
> **高分回答：** "不一定。HTTP/2 的多路复用解决了 HTTP 层的队头阻塞，但 TCP 层的队头阻塞仍在——一个 TCP 包丢失会阻塞该连接上所有流。在高丢包率的网络环境下（如移动网络），HTTP/2 可能比 HTTP/1.1 更慢，因为 HTTP/1.1 的 6 个连接是独立的，一个连接丢包不影响其他连接。HTTP/3 用 QUIC（基于 UDP）为每个 Stream 独立处理丢包，彻底解决了这个问题。"

> [accordion] DNS 递归查询和迭代查询有什么区别？浏览器用哪种？
>
> **答题要点：** 浏览器到本地 DNS 是递归查询，本地 DNS 到根/权威 DNS 是迭代查询。
>
> **高分回答：** "递归查询是'我帮你查到底'——浏览器问本地 DNS，本地 DNS 负责从根 DNS 一直问到权威 DNS，最后把结果返回给浏览器。迭代查询是'我告诉你下一步问谁'——根 DNS 不直接查，而是告诉本地 DNS '你去问 .com DNS'，本地 DNS 再问 .com DNS，.com DNS 说'你去问 example.com 权威 DNS'，最后权威 DNS 返回结果。浏览器到本地 DNS 是递归查询，本地 DNS 到根/权威 DNS 是迭代查询。"

> [accordion] TCP 三次握手和四次挥手的过程是什么？为什么不能两次握手？
>
> **答题要点：** 三次握手同步初始序列号并确认收发能力，四次挥手确保双方数据都发完。
>
> **高分回答：** "三次握手：客户端发 SYN（Seq=x），服务器回 SYN+ACK（Seq=y, Ack=x+1），客户端发 ACK（Ack=y+1）。为什么不能两次？因为服务器回复 SYN+ACK 时，客户端还没确认自己能收到服务器的数据——如果只握手两次，服务器不知道客户端能不能收到自己的回复，可能导致服务器单方面建立连接。四次挥手：客户端发 FIN，服务器回 ACK（此时服务器可能还有数据没发完），服务器发 FIN，客户端回 ACK。为什么是四次而不是三次？因为 TCP 是全双工的——关闭发送通道和接收通道是独立的操作。"

> [accordion] TLS 握手过程是什么？TLS 1.2 和 1.3 有什么区别？
>
> **答题要点：** TLS 1.2 需要 2-RTT，TLS 1.3 优化为 1-RTT，支持 0-RTT 恢复。
>
> **高分回答：** "TLS 1.2 握手：客户端发 ClientHello（支持的加密套件列表），服务器回 ServerHello（选定的加密套件）+ 证书，客户端验证证书 + 交换密钥，双方生成会话密钥。需要 2-RTT。TLS 1.3 优化：将密钥交换和证书验证合并到前两条消息，客户端在 ClientHello 时就附带 DH 密钥共享，服务器在 ServerHello 时就完成密钥交换。只需 1-RTT。还支持 0-RTT 恢复——通过 Pre-Shared Key 在第一个数据包中就携带应用数据，但有重放攻击风险。"

> [accordion] Composite 阶段的优化原理是什么？为什么 transform/opacity 动画性能最好？
>
> **答题要点：** Composite 阶段由 GPU 合成器执行，只需要矩阵变换，不需要重排重绘。
>
> **高分回答：** "Composite 阶段由 GPU 合成器将多个图层（Composited Layers）合并为最终画面。当元素被提升为独立图层后（通过 `will-change` 或 `transform`），后续的 `transform`/`opacity` 变化只需要 GPU 做矩阵变换，不需要重排（Layout）也不需要重绘（Paint）。
>
> ```javascript theme={null}
> // 只触发 Composite，不触发 Layout 和 Paint
> element.style.transform = "translateX(100px)";
> element.style.opacity = 0.5;
>
> // 触发 Layout + Paint + Composite，性能差
> element.style.left = "100px";
> element.style.width = "200px";
> ```
>
> 优化策略：用 `transform` 做位移/缩放/旋转动画，用 `opacity` 做透明度动画，避免触发布局属性（`width`、`height`、`margin`、`padding`）变化。"

***

## 延伸阅读与引用参考

* [How Browsers Work](https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/)：Tali Garsiel & Paul Irish 的经典文章，系统梳理了解析、渲染树、布局与绘制的完整流程，是浏览器原理文章的重要参考。<sup>\[1]</sup>
* [High Performance Browser Networking](https://hpbn.co/)：Ilya Grigorik 的免费在线书籍，涵盖 DNS、TCP、TLS、HTTP 全链路优化，是网络层性能优化的权威参考。<sup>\[2]</sup>
* [Inside look at modern web browser](https://developer.chrome.com/blog/inside-browser-part1)：Lin Clark 在 Chrome Developers 发表的四部曲系列（2018），用漫画解析 Chromium 多进程架构、渲染流水线和合成器工作原理。
* 李兵 [极客时间《浏览器工作原理与实践》](https://time.geekbang.org/column/intro/100033601)：将渲染流水线拆解为 Style → Layout → Paint → Composite 四阶段，给出每帧 16.6ms 的时间分配模型（JS 5ms + 渲染 11ms）。
* [《HTTP 图解》](https://book.douban.com/subject/25863515/)：上野宣著，用图解方式讲解 HTTP 协议的核心知识。
* [MDN Web Docs - 渲染性能](https://developer.mozilla.org/en-US/docs/Web/Performance)：MDN 对浏览器渲染性能的权威指南。
* [Cloudflare Blog - HTTP/3](https://blog.cloudflare.com/http-3-the-past-present-and-future/)：Cloudflare 对 HTTP/3 和 QUIC 协议的深度解析，含 0-RTT 恢复的安全风险分析。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.