浏览器渲染流程和性能
浏览器渲染流程和性能优化相关内容
本笔记系统梳理从浏览器地址栏输入 URL 到页面最终渲染完成的完整流程
一、整体流程概览
当用户在浏览器地址栏输入一个 URL 并回车后,浏览器会经历以下关键阶段:
| 阶段 | 主要工作 | 涉及协议/技术 |
|---|---|---|
| 1. URL 解析 | 解析 URL 结构,判断协议、域名、端口、路径 | URL 规范 |
| 2. DNS 解析 | 将域名转换为 IP 地址 | DNS、UDP |
| 3. TCP 连接 | 建立可靠的传输层连接(三次握手) | TCP |
| 4. 发送 HTTP 请求 | 构造请求报文并发送 | HTTP/HTTPS |
| 5. 服务器处理 | 后端处理请求并返回响应报文 | HTTP |
| 6. 浏览器渲染 | 解析 HTML/CSS/JS,构建 DOM 树并绘制页面 | 浏览器渲染引擎 |
| 7. 断开连接 | 关闭 TCP 连接(四次挥手) | TCP |
二、URL 输入与浏览器解析
2.1 URL 的组成结构
以 https://www.example.com:443/path/page?id=1#section 为例:
https:// www.example.com :443 /path/page ?id=1 #section
↓ ↓ ↓ ↓ ↓ ↓
协议 域名(host) 端口 路径(path) 查询参数 锚点
- 协议(Protocol):决定使用什么方式传输数据(http / https / ftp 等)
- 域名(Host):服务器地址,需通过 DNS 解析为 IP
- 端口(Port):http 默认 80,https 默认 443
- 路径(Path):服务器上资源的具体位置
- 查询参数(Query):以
?开头,键值对形式传递参数 - 锚点(Hash):以
#开头,不会发送到服务器,仅用于前端定位
2.2 浏览器输入后的预处理
- 判断输入内容:是搜索关键词还是合法 URL?若不是合法 URL,则交给默认搜索引擎
- HSTS 检查:检查域名是否在 HSTS(HTTP Strict Transport Security)列表中,若在则强制升级为 HTTPS
- 检查缓存:浏览器按以下顺序查找资源
- Service Worker 缓存
- Memory Cache(内存缓存)
- Disk Cache(磁盘缓存)
- Push Cache(HTTP/2 推送缓存)
- 构造完整请求:补全协议、端口等缺省信息
💡 提示: 若强缓存(Cache-Control / Expires)命中,浏览器会直接使用本地资源,根本不会发起网络请求。
三、DNS 解析
3.1 什么是 DNS
DNS(Domain Name System,域名系统)的作用是将人类可读的域名(如 www.example.com)翻译成机器可识别的 IP 地址(如 93.184.216.34)。
3.2 DNS 解析的完整流程
DNS 查询采用递归查询 + 迭代查询结合的方式,查找顺序如下:
1. 浏览器 DNS 缓存
↓ 未命中
2. 操作系统 DNS 缓存(包括 hosts 文件)
↓ 未命中
3. 本地 DNS 服务器(通常是运营商 ISP 提供)
↓ 未命中
4. 根域名服务器(Root DNS Server)
↓ 返回顶级域服务器地址
5. 顶级域名服务器(TLD,如 .com / .cn)
↓ 返回权威域名服务器地址
6. 权威域名服务器(Authoritative DNS Server)
↓ 返回最终 IP
7. 拿到 IP,逐级缓存返回给浏览器
3.3 DNS 优化策略
| 优化手段 | 原理 | 效果 |
|---|---|---|
| DNS 缓存 | 浏览器、操作系统、本地 DNS 多级缓存 | 减少查询时间 |
| DNS Prefetch | 提前对页面中可能用到的域名进行解析 | 提前完成解析,节省主请求时间 |
| HTTPDNS | 用 HTTP 协议替代 UDP 向 DNS 服务器查询 | 避免 DNS 劫持,结果更准确 |
| 减少域名数量 | 合并资源到同一个域名下 | 减少 DNS 查询次数 |
| 使用 CDN | 就近返回边缘节点 IP | 降低延迟 |
DNS Prefetch 用法示例:
<!-- 在 head 中预解析指定域名,浏览器会异步解析这些域名 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//api.example.com">
<!-- preconnect 更进一步:除了 DNS 还会建立 TCP 连接 -->
<link rel="preconnect" href="//cdn.example.com" crossorigin>
⚠️ 注意: DNS 解析默认使用 UDP 协议(端口 53),因为 UDP 无连接、速度快;只有数据包过大时才会切换到 TCP。
四、TCP 连接:三次握手与四次挥手
4.1 三次握手(建立连接)
TCP 是面向连接、可靠的传输层协议。在发送数据前必须先建立连接,过程称为"三次握手"。
客户端 服务端
│ │
│ ───── 1. SYN (seq=x) ────────────►│ [LISTEN]
│ │
│ [SYN_RCVD]
│ ◄──── 2. SYN + ACK (seq=y, ack=x+1) ────────────│
│ │
[ESTABLISHED] │
│ ───── 3. ACK (seq=x+1, ack=y+1) ────────────►│ [ESTABLISHED]
│ │
│ 连接建立,开始传输数据 │
三步详解:
- 第一次握手:客户端发送 SYN(同步)报文,请求建立连接
- 第二次握手:服务端回复 SYN + ACK,表示同意建立连接
- 第三次握手:客户端再次发送 ACK,连接正式建立
为什么是三次而不是两次?
为了防止已失效的连接请求报文突然到达服务端,使服务端建立无效连接,造成资源浪费。三次握手能确保双方都具备发送和接收能力。
4.2 四次挥手(断开连接)
客户端 服务端
│ │
│ ───── 1. FIN (seq=u) ────────────►│
[FIN_WAIT_1] │
│ [CLOSE_WAIT]
│ ◄──── 2. ACK (ack=u+1) ────────────│
[FIN_WAIT_2] │
│ │
│ (服务端还可能有数据要发送) │
│ │
│ ◄──── 3. FIN (seq=w) ────────────│ [LAST_ACK]
│ │
[TIME_WAIT] │
│ ───── 4. ACK (ack=w+1) ────────────►│ [CLOSED]
│ │
│ 等待 2*MSL 后关闭 │
[CLOSED]
四步详解:
- 第一次挥手:客户端发送 FIN,表示数据已发完,请求断开
- 第二次挥手:服务端返回 ACK,表示收到关闭请求(但服务端可能还有数据要传)
- 第三次挥手:服务端处理完后发送 FIN,表示自己也要关闭
- 第四次挥手:客户端返回 ACK,进入 TIME_WAIT 状态,等待 2*MSL 后真正关闭
为什么挥手要四次?
因为 TCP 是全双工的,关闭连接需要双方各自确认。服务端收到 FIN 后不能立刻关闭,可能还有数据没发完,所以 ACK 和 FIN 分两次发送。
🚨 警告: TIME_WAIT 状态会持续 2*MSL(Maximum Segment Lifetime,通常 60s 左右),高并发场景下可能导致端口耗尽。
4.3 具体实现过程
本节从 TCP 协议实现层面补充 4.1 / 4.2 的抽象描述,关注报文标志位与状态流转细节。
TCP 传输服务
TCP(Transmission Control Protocol,传输控制协议)提供面向连接的、可靠的字节流服务。这句话拆开来理解:
| 特性 | 含义 |
|---|---|
| 面向连接 | 通信前必须先建立连接(三次握手),通信后必须释放连接(四次挥手),类似打电话要先拨号、最后挂断 |
| 可靠传输 | 通过 Sequence Number(序号)、ACK(确认应答)、超时重传等机制,保证数据不丢、不乱序、不重复 |
| 字节流服务 | 数据被视为无结构的字节序列,由 TCP 负责拆包、组包,上层应用不需要关心分片细节 |
三次握手 — 实现细节
每一步都涉及 TCP 标志位和序列号的精确控制:
-
第一次握手
- 客户端发送 SYN 包:
SYN=1,序列号Seq=x(随机生成) - 客户端进入 SYN_SEND 状态,等待服务端确认
-
SYN(Synchronize Sequence Number):同步序列号,用于发起连接请求
- 客户端发送 SYN 包:
-
第二次握手
- 服务端收到 SYN 包后,回复 SYN + ACK 包
- 其中
SYN=1, ACK=1,确认号Ack=x+1,同时自己也发送序列号Seq=y - 服务端进入 SYN_RECV(同步已接收)状态
-
ACK(Acknowledgment):确认应答,表示已收到对方的报文
-
第三次握手
- 客户端收到 SYN+ACK 包后,向服务端发送确认包 ACK
- 其中
ACK=1,确认号Ack=y+1 - 此包发送完毕后,双方进入 ESTABLISHED(已建立)状态,完成三次握手
💡 提示: SYN 包中的序列号
x和y均为随机生成,目的是防止序列号预测攻击(Sequence Number Prediction Attack)。
四次挥手 — 实现细节
四次挥手的每一步同样依赖标志位和序列号控制状态流转:
-
第一次挥手
- 客户端(主动关闭方)发送 FIN 包:
FIN=1,序列号Seq=u - 表示客户端 → 服务端方向的数据传输已结束
- 客户端进入 FIN_WAIT_1(终止等待-1)状态
-
FIN(Finish):结束标志,表示发送方数据已发完,请求释放连接
- 客户端(主动关闭方)发送 FIN 包:
-
第二次挥手
- 服务端收到 FIN 包后,发送 ACK 确认包:
ACK=1,确认号Ack=u+1,同时携带自己的序列号Seq=v - 此时客户端 → 服务端方向已关闭,但服务端 → 客户端方向仍可继续发送数据
- 服务端进入 CLOSE_WAIT(关闭等待)状态;客户端收到 ACK 后进入 FIN_WAIT_2 状态
- 服务端收到 FIN 包后,发送 ACK 确认包:
-
第三次挥手
- 服务端将剩余数据发送完毕后,发送 FIN 包:
FIN=1,序列号Seq=w - 表示服务端也准备好关闭连接
- 服务端进入 LAST_ACK(最后确认)状态,等待客户端最终确认
- 服务端将剩余数据发送完毕后,发送 FIN 包:
-
第四次挥手
- 客户端收到 FIN 后,发送 ACK 确认包:
ACK=1,确认号Ack=w+1 - 客户端进入 TIME_WAIT(时间等待)状态,等待 2 × MSL 后进入 CLOSED
- 服务端收到 ACK 后立即进入 CLOSED 状态
- 客户端收到 FIN 后,发送 ACK 确认包:
🚨 关键:
MSL(Maximum Segment Lifetime,最大报文生存时间),通常为 30s ~ 60s。客户端等待 2 × MSL 的原因有两点:
- 确保最后一个 ACK 能被服务端收到(若丢失,服务端会在 2MSL 内重发 FIN)
- 让旧连接的所有残余报文在网络中彻底消失,避免干扰同一端口上的新连接
4.4 HTTPS 的额外步骤:TLS 握手
如果是 HTTPS 请求,在 TCP 三次握手之后还需要进行 TLS 握手:
| 步骤 | 内容 |
|---|---|
| 1. ClientHello | 客户端发送支持的 TLS 版本、加密套件、随机数 |
| 2. ServerHello | 服务端选择加密套件,返回证书和随机数 |
| 3. 证书验证 | 客户端验证服务端证书合法性 |
| 4. 密钥交换 | 通过非对称加密协商出对称加密密钥 |
| 5. Finished | 双方完成握手,开始用对称加密通信 |
五、发送 HTTP 请求
5.1 HTTP 请求报文结构
GET /index.html HTTP/1.1 ← 请求行(方法 + 路径 + 协议版本)
Host: www.example.com ← 请求头
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml
Accept-Encoding: gzip, deflate, br
Cookie: sessionId=abc123
← 空行(分隔头部与正文)
(请求体,GET 通常为空) ← 请求体
5.2 常见请求方法对比
| 方法 | 用途 | 幂等性 | 安全性 | 是否有请求体 |
|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | ❌ |
| POST | 创建资源 | ❌ | ❌ | ✅ |
| PUT | 全量更新资源 | ✅ | ❌ | ✅ |
| PATCH | 部分更新资源 | ❌ | ❌ | ✅ |
| DELETE | 删除资源 | ✅ | ❌ | ❌ |
| HEAD | 仅获取响应头 | ✅ | ✅ | ❌ |
| OPTIONS | 查询服务端支持的方法 | ✅ | ✅ | ❌ |
💡 提示: 幂等性指多次请求与一次请求的效果相同;安全性指请求不会改变服务端状态。
5.3 HTTP缓存
5.3.1 强缓存
-
强缓存如果命中不会向服务器发送请求,直接读取本地数据;
-
状态码返回 200 (from memory cache) 或者 200 (from disk cache)
-
from memory cache 内存缓存,小文件或者频繁访问的文件
-
from disk cache 磁盘缓存,一般存的大文件
-
强缓存可以通过两种响应头来配置:Cache-Control 和 Expires;
-
**Cache-Control:**max-age=缓存多少秒
-
Expires:过期时间
注意:如果同时设置了Cache-Control 和 Expires 时 Cache-Control 优先级更高,因为 Expires使用的是具体的日期和时间,客户端和服务器之间可能存在时间不同步的情况,Cache-Control 设置的是缓存的秒数,不存在这种问题。
5.3.2 协商缓存
描述: 当客户端请求资源时,如果该资源已经被缓存在本地,客户端会向服务器发送一个带有本地缓存的相关信息的请求(如Last-Modified和ETag),询问服务器资源是否有更新,如果有更新的话就返回最新的资源,没有更新时返回 304 状态码,通知浏览器可以使用本地缓存的资源。
具体的工作原理如下:
- 当客户端首次请求某个资源时,服务器会在响应头中携带文件最后修改时间(Last-Modified)或唯一内容标识(ETag)。
- 当客户端再次请求相同的资源时,会在请求头中把之前服务器返回的 Last-Modified 或 ETag 通过 If-Modified-Since 和 If-None-Match 字段传给服务器。
- If-Modified-Since 传 Last-Modified
- If-None-Match 传 ETag
- 服务器接收到请求后,会比较客户端传入的 Last-Modified 或 ETag 与服务器上的是否一致。
- 如果两者一致,服务器会返回 304 状态码,浏览器可以直接从本地缓存中读取。
- 如果不一致,服务器会返回 200,和最新的资源内容。
5.3.3 协商缓存为什么有 Last-Modified 还要有 ETag
协商缓存中同时使用 Last-Modified 和 ETag 是为了解决单一机制存在的局限性,两者互为补充。以下是主要原因:
Last-Modified 的局限性
- 精度问题:Last-Modified 只能精确到秒级,如果文件在1秒内多次修改无法识别
- 内容不变时间变:文件内容没变但修改时间变了(如touch操作),会导致不必要的重新下载
- 无法识别重命名:文件被重命名但内容相同,Last-Modified 会认为资源已改变
- 分布式系统时间同步问题:不同服务器时间不一致可能导致缓存失效判断错误
ETag 的优势
- 更精确的变更检测:ETag 是基于文件内容生成的哈希值或版本号,能精确识别内容变化
- 解决时间精度问题:不受时间精度限制,能检测1秒内的多次修改
- 处理特殊情况:能正确处理仅修改时间但内容不变的情况
- 分布式系统友好:不依赖服务器时间,适合分布式环境
实际应用中的配合
- 服务器通常同时发送 Last-Modified 和 ETag
- 客户端在后续请求中可能同时发送 If-Modified-Since 和 If-None-Match
- 服务器优先验证 ETag,若匹配则直接返回304,不匹配再检查 Last-Modified
- 这种双重验证机制提高了缓存判断的准确性
5.3.4 浏览器加载缓存资源的流程
- 浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用本地资源;
- 如果不命中则根据头信息向服务器发起请求,服务器判断是否命中协商缓存;
- 如果协商缓存命中的话,则服务器状态码返回304,不返回资源,浏览器直接使用本地资源;
- 如果协商缓存不命中,则服务器返回最新的资源给浏览器。
5.4 HTTP/1.1 vs HTTP/2 vs HTTP/3
了解 HTTP 版本演进,是网络优化的基础。
HTTP/1.1 的问题
| 问题 | 说明 |
|---|---|
| 队头阻塞(HOL Blocking) | 同一连接内请求串行发送,前一个慢了后面全等 |
| 连接数限制 | 浏览器对同一域名默认最多 6 个并发连接 |
| Header 冗余 | 每次请求都携带完整 Header,无压缩 |
| 无服务端推送 | 服务端只能被动响应,不能主动推送资源 |
HTTP/2 的核心改进
| 特性 | 说明 |
|---|---|
| 多路复用(Multiplexing) | 单个 TCP 连接中并行处理多个请求/响应,彻底解决队头阻塞 |
| Header 压缩(HPACK) | 使用 HPACK 算法压缩 Header,减少冗余传输 |
| 服务端推送(Server Push) | 服务端可主动推送客户端需要的资源(如 CSS、JS) |
| 二进制分帧(Binary Framing) | 将数据分割为二进制帧传输,解析效率更高 |
| 请求优先级 | 可以给请求设置优先级,重要资源优先传输 |
HTTP/1.1:连接 A → [请求1] → [响应1] → [请求2] → [响应2](串行)
HTTP/2:
连接 A → [请求1] [请求2] [请求3] (并行,多路复用)
↓ ↓ ↓
[响应1] [响应2] [响应3]
HTTP/3
HTTP/3 基于 QUIC 协议(基于 UDP),解决了 HTTP/2 的 TCP 层队头阻塞:
| HTTP/2 | HTTP/3 |
|---|---|
| 基于 TCP(可靠但有 TCP 队头阻塞) | 基于 QUIC/UDP(UDP 层面实现可靠性) |
| TLS 握手 + TCP 握手分开 | 0-RTT 或 1-RTT 建立连接(更快) |
| 网络切换需重建连接 | Connection ID 机制,网络切换无感知 |
💡 实际影响: HTTP/2 在弱网环境下可能比 HTTP/1.1 更慢(一个 TCP 包丢失影响所有流);HTTP/3 的 QUIC 从根本上解决了这个问题。
六、服务器处理请求并返回响应
6.1 服务器处理流程
1. 接收 HTTP 请求
↓
2. 反向代理(如 Nginx)转发到应用服务器
↓
3. 应用服务器(Tomcat / Node.js / FastAPI 等)路由匹配
↓
4. 中间件处理(鉴权、日志、限流等)
↓
5. 业务逻辑处理(查询数据库、调用其他服务)
↓
6. 构造响应报文返回
6.2 HTTP 响应报文结构
HTTP/1.1 200 OK ← 状态行(协议 + 状态码 + 状态描述)
Content-Type: text/html; charset=UTF-8 ← 响应头
Content-Length: 1024
Cache-Control: max-age=3600
Set-Cookie: sessionId=xyz789
← 空行
<!DOCTYPE html> ← 响应体
<html>...</html>
6.3 常见 HTTP 状态码
| 状态码 | 类别 | 含义 |
|---|---|---|
| 1xx | 信息响应 | 请求已被接收,继续处理 |
| 200 | 成功 | 请求成功 |
| 204 | 成功 | 无内容 |
| 301 | 重定向 | 永久重定向 |
| 302 | 重定向 | 临时重定向 |
| 304 | 重定向 | 资源未修改,使用本地缓存 |
| 400 | 客户端错误 | 请求语法错误 |
| 401 | 客户端错误 | 未认证 |
| 403 | 客户端错误 | 禁止访问 |
| 404 | 客户端错误 | 资源不存在 |
| 500 | 服务端错误 | 服务器内部错误 |
| 502 | 服务端错误 | 网关错误 |
| 503 | 服务端错误 | 服务不可用 |
| 504 | 服务端错误 | 网关超时 |
七、浏览器解析与渲染页面
7.1 渲染流程总览
浏览器渲染引擎(如 Blink、WebKit)拿到 HTML 后,会经过以下关键步骤:
HTML ──解析──► DOM 树
│
CSS ──解析──► CSSOM 树
│
▼
Render 树(渲染树)
│
▼
Layout(布局/回流)
│
▼
Paint(绘制)
│
▼
Composite(合成)
│
▼
显示到屏幕
7.2 关键步骤详解
1. 构建 DOM 树
浏览器按字节 → 字符 → Token → Node → DOM Tree 的顺序解析 HTML。
<!-- 这段 HTML 会被解析为对应的 DOM 树 -->
<html>
<head><title>Demo</title></head>
<body>
<p>Hello</p>
</body>
</html>
2. 构建 CSSOM 树
CSS 解析过程中,浏览器会阻塞渲染,因为 CSS 影响元素样式。
3. 构建 Render 树
将 DOM 和 CSSOM 合并,不可见的元素(如 display:none、<head>)不会进入渲染树。
4. Layout(布局/回流 Reflow)
计算每个节点在屏幕上的精确位置和大小。
5. Paint(绘制 Repaint)
将每个节点绘制成像素。
6. Composite(合成)
将多个图层合并成最终页面,由 GPU 加速完成。
7.3 回流(Reflow)与重绘(Repaint)
| 对比项 | 回流 Reflow | 重绘 Repaint |
|---|---|---|
| 触发条件 | 元素几何属性变化(宽高、位置等) | 元素外观变化(颜色、背景) |
| 是否影响布局 | ✅ | ❌ |
| 性能开销 | 大 | 小 |
| 是否触发重绘 | ✅ 一定会 | ❌ |
🚨 警告: 回流必然引起重绘,但重绘不一定引起回流。频繁回流是性能优化的重点。
减少回流/重绘的方法:
// ❌ 多次操作 DOM,触发多次回流
const list = document.getElementById('list')
for (let i = 0; i < 100; i++) {
list.innerHTML += `<li>${i}</li>` // 每次都会触发回流
}
// ✅ 使用 DocumentFragment 批量操作
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
const li = document.createElement('li')
li.textContent = i
fragment.appendChild(li) // 仅在内存中操作
}
list.appendChild(fragment) // 只触发一次回流
// ✅ 使用 CSS class 替代多次修改 style
// 推荐写法:合并样式变更
element.classList.add('active') // 一次回流
// ❌ 不推荐:多次修改 style
element.style.width = '100px' // 触发回流
element.style.height = '100px' // 再次触发回流
element.style.margin = '10px' // 又一次触发回流
八、JS 解析与执行
8.1 JS 解析的整体流程
JS 源代码
↓
词法分析(Tokenizing/Lexing)
↓
语法分析(Parsing)→ 生成 AST(抽象语法树)
↓
预解析(变量提升、函数提升)
↓
字节码生成
↓
解释执行 / JIT 编译为机器码
8.2 预解析(变量提升)
JS 引擎在执行代码前会先扫描整个作用域,将 变量声明和函数声明提升到顶部。
// 原始代码
console.log(a) // 输出 undefined(不是报错)
console.log(b) // 输出 ƒ b() {}(函数被完整提升)
var a = 1
function b() {}
// 等价于经过预解析后的代码
var a // 变量声明被提升,但未赋值
function b() {} // 函数声明被整体提升
console.log(a) // undefined
console.log(b) // ƒ b() {}
a = 1 // 赋值留在原位置
⚠️ 注意: let 和 const 不会被提升,存在"暂时性死区"(TDZ),在声明前访问会抛出 ReferenceError。
// let / const 的暂时性死区
console.log(x) // ❌ ReferenceError: Cannot access 'x' before initialization
let x = 10
// var 的变量提升
console.log(y) // ✅ undefined
var y = 10
8.3 浏览器引擎执行 JS
主流 JS 引擎:
| 引擎 | 浏览器 |
|---|---|
| V8 | Chrome、Edge、Node.js |
| SpiderMonkey | Firefox |
| JavaScriptCore | Safari |
V8 执行流程:
源代码
↓
Parser(解析)→ AST
↓
Ignition(解释器)→ 字节码 → 立即执行
↓ ↓
↓ 收集运行时反馈
↓ ↓
TurboFan(优化编译器)← 热点代码标记
↓
高度优化的机器码
8.4 JS 与页面渲染的关系
JS 是单线程的,且会阻塞 DOM 解析。
<!-- 默认行为:阻塞 DOM 解析,立即下载并执行 -->
<script src="app.js"></script>
<!-- async:异步下载,下载完成后立即执行(仍会阻塞解析) -->
<script async src="app.js"></script>
<!-- defer:异步下载,等 HTML 解析完成后再执行 -->
<script defer src="app.js"></script>
三者对比:
| 属性 | 下载 | 执行时机 | 是否阻塞 HTML 解析 | 执行顺序 |
|---|---|---|---|---|
| 无属性 | 同步 | 立即执行 | ✅ 阻塞 | 按文档顺序 |
| async | 异步 | 下载完立即执行 | 执行时阻塞 | 不保证顺序 |
| defer | 异步 | DOMContentLoaded 之前 | ❌ 不阻塞 | 按文档顺序 |
💡 提示: 推荐将不影响首屏的脚本放到 body 底部,或使用 defer 属性,避免阻塞页面渲染。
8.5 完整渲染时间线示例
时间轴 ──────────────────────────────────────────────►
[HTML 下载] [解析 HTML 构建 DOM]
↓
遇到 <link rel="stylesheet">
↓
[CSS 下载] [构建 CSSOM] ← 阻塞渲染
↓
遇到 <script>
↓
[JS 下载] [JS 执行] ← 阻塞 DOM 解析
↓
继续解析 HTML
↓
[构建 Render 树]
↓
[Layout]
↓
[Paint]
↓
[Composite]
↓
DOMContentLoaded 事件触发
↓
[加载图片等外部资源]
↓
load 事件触发
九、性能优化要点总结
| 阶段 | 优化手段 |
|---|---|
| 网络 | DNS Prefetch、Preconnect、CDN、HTTP/2、缓存策略 |
| TCP | Keep-Alive 复用连接、减少 HTTPS 握手开销 |
| HTTP | Gzip / Brotli 压缩、合并请求、雪碧图 |
| HTML | 减少 DOM 节点、合理嵌套 |
| CSS | 避免深层选择器、关键 CSS 内联、避免 @import |
| JS | defer / async、代码分割、Tree Shaking、Web Worker |
| 渲染 | 减少回流重绘、使用 transform / opacity 触发 GPU 加速 |
| 图片 | 懒加载、WebP/AVIF 格式、响应式图片 |
💡 最佳实践: 理解整个流程是性能优化的基础——只有知道时间花在哪里,才能针对性地优化。
十、面试高频问答
Q1:从输入 URL 到看到页面,发生了什么?
可以按照"URL 解析 → DNS → TCP → HTTPS → HTTP → 服务器处理 → 浏览器渲染 → 断开连接"这条主线展开回答,每一步都能再深入。
Q2:DNS 解析为什么用 UDP?
UDP 无连接、报文头小、速度快,适合短小的查询请求;数据量大时会切换到 TCP。
Q3:为什么 TCP 是三次握手而不是两次或四次?
两次无法确认客户端的接收能力,可能建立无效连接;四次则多余,第二次握手中 SYN 和 ACK 可以合并。
Q4:defer 和 async 有什么区别?
参考上文 8.4 节中的对比表格。
Q5:如何减少回流和重绘?
批量修改 DOM、使用 class 代替 style、脱离文档流(absolute / fixed)、利用 transform 触发 GPU 合成层。