1155 字
3 分钟
浏览器渲染原理:从 URL 到页面
前言
“从输入 URL 到页面展示发生了什么?” 是前端面试中最经典的问题之一。这个问题看似简单,实则涉及网络协议、浏览器架构、渲染引擎等多个领域的知识。本文将系统性地剖析整个流程,让你对浏览器工作原理有深入理解。
全流程概览
flowchart TB
subgraph 网络层
A[URL 解析] --> B[DNS 查询]
B --> C[TCP 连接]
C --> D[TLS 握手]
D --> E[HTTP 请求]
E --> F[接收响应]
end
subgraph 解析层
G[HTML 解析] --> H[构建 DOM 树]
H --> I[CSS 解析]
I --> J[构建 CSSOM 树]
J --> K[执行 JavaScript]
end
subgraph 渲染层
L[构建渲染树] --> M[布局计算]
M --> N[绘制]
N --> O[合成]
O --> P[显示]
end
F --> G
K -.-> H
一、网络层
1.1 URL 解析
浏览器首先对 URL 进行解析:
https://www.example.com:443/path/to/page?query=value#hash└─┬──┘ └───────┬───────┘ ┊ ┊ └─────┬─────┘ └───┬───┘ 协议 域名 端口 路径 查询参数 锚点| 组件 | 示例 | 说明 |
|---|---|---|
| 协议 | https | 决定使用 HTTP 还是 HTTPS |
| 域名 | www.example.com | 需要解析为 IP 地址 |
| 端口 | 443 | HTTP 默认 80,HTTPS 默认 443 |
| 路径 | /path/to/page | 请求资源路径 |
| 查询 | ?query=value | 传递参数 |
| 锚点 | #hash | 页面内定位 |
1.2 DNS 查询
DNS 解析将域名转换为 IP 地址:
sequenceDiagram
participant B as 浏览器缓存
participant S as 系统缓存
participant R as 路由器缓存
participant L as 本地 DNS
participant R as 根 DNS
participant A as 权威 DNS
Note over B: 检查浏览器缓存
B-->>B: 未命中
Note over S: 检查系统缓存
S-->>S: 未命中
S->>L: 递归查询请求
L->>R: 查询 .com NS
R-->>L: 返回 .com NS 地址
L->>A: 查询 example.com
A-->>L: 返回 IP 地址
L-->>S: 返回 IP 地址
DNS 缓存层级:
| 缓存位置 | TTL | 说明 |
|---|---|---|
| 浏览器缓存 | 1-60 秒 | 最快,容量有限 |
| 系统缓存 | 根据 DNS 设置 | OS 级别缓存 |
| 路由器缓存 | 不定 | 家用路由器 |
| ISP DNS | 根据 DNS 设置 | 运营商缓存 |
1.3 TCP 连接
建立 TCP 连接需要三次握手:
sequenceDiagram
participant C as 客户端
participant S as 服务端
Note over C: SYN_SENT
C->>S: SYN=1, seq=x
Note over S: SYN_RCVD
S->>C: SYN=1, ACK=1, seq=y, ack=x+1
Note over C: ESTABLISHED
C->>S: ACK=1, seq=x+1, ack=y+1
Note over S: ESTABLISHED
| 状态 | 说明 |
|---|---|
| SYN_SENT | 发送 SYN 后等待 |
| SYN_RCVD | 收到 SYN,发送 SYN+ACK |
| ESTABLISHED | 连接建立完成 |
TCP Fast Open (TFO):
现代浏览器支持 TCP Fast Open,可以在第三次握手时携带数据:
首次连接:正常三次握手,获取 Cookie后续连接:SYN + Cookie + 数据 → 跳过等待1.4 TLS 握手
HTTPS 连接需要 TLS 握手(以 TLS 1.3 为例):
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello + Key Share
Note over C: 计算共享密钥
S->>C: ServerHello + Key Share + Certificate + Finished
Note over S: 计算共享密钥
C->>S: Finished
Note over C,S: 加密通信开始
TLS 1.3 改进:
| 特性 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手 RTT | 2 | 1 |
| 0-RTT | 不支持 | 支持 |
| 密码套件 | 多种 | 仅 AEAD |
| 前向保密 | 可选 | 强制 |
1.5 HTTP 请求
连接建立后发送 HTTP 请求:
GET /path/to/page HTTP/1.1Host: www.example.comUser-Agent: Mozilla/5.0 ...Accept: text/html,application/xhtml+xmlAccept-Language: zh-CN,zh;q=0.9Accept-Encoding: gzip, deflate, brConnection: keep-aliveCookie: session_id=abc123HTTP/2 多路复用:
flowchart LR
subgraph HTTP/1.1
R1[请求1] --> R2[请求2]
R2 --> R3[请求3]
end
subgraph HTTP/2
S1[流1]
S2[流2]
S3[流3]
S1 -.-> S2 -.-> S3
end
1.6 接收响应
服务器返回响应:
HTTP/1.1 200 OKContent-Type: text/html; charset=utf-8Content-Encoding: gzipCache-Control: max-age=3600Transfer-Encoding: chunked
<!DOCTYPE html><html>...二、解析层
2.1 HTML 解析
浏览器使用 HTML 解析器将字节流转换为 DOM 树:
flowchart LR
A[字节流] --> B[字符流]
B --> C[词法分析 Token]
C --> D[语法分析]
D --> E[DOM 树]
解析过程:
输入: <div id="app">Hello</div>
1. 词法分析生成 Token: - StartTag: div, attributes: {id: "app"} - Character: "Hello" - EndTag: div
2. 语法分析构建树: Document └── HTMLHtmlElement └── HTMLBodyElement └── HTMLDivElement (id="app") └── Text: "Hello"预扫描优化:
浏览器会预扫描 HTML,提前发现需要加载的资源:
<html> <head> <!-- 预扫描发现 stylesheet,提前发起请求 --> <link rel="stylesheet" href="style.css" /> <!-- 预扫描发现 script,但会等待 HTML 解析完 --> <script src="app.js"></script> </head> ...</html>2.2 阻塞与异步
不同资源对解析的影响:
| 资源类型 | 是否阻塞解析 | 是否阻塞渲染 |
|---|---|---|
| CSS | 否 | 是 |
| 同步 JS | 是 | 是 |
| async JS | 否 | 否 |
| defer JS | 否 | 否 |
| 图片 | 否 | 否 |
<!-- 阻塞解析 --><script src="blocking.js"></script>
<!-- 异步加载,不阻塞解析 --><script async src="analytics.js"></script>
<!-- 延迟执行,HTML 解析完后执行 --><script defer src="app.js"></script>
<!-- CSS 阻塞渲染 --><link rel="stylesheet" href="style.css" />2.3 构建 DOM 树
DOM 树的构建过程:
// 简化的 DOM 树结构const domTree = { nodeType: 9, // Document childNodes: [ { nodeType: 1, // Element nodeName: 'HTML', childNodes: [ { nodeType: 1, nodeName: 'HEAD', childNodes: [...] }, { nodeType: 1, nodeName: 'BODY', childNodes: [...] } ] } ]};2.4 构建 CSSOM 树
CSS 解析为 CSSOM(CSS Object Model):
/* 输入 CSS */body { font-size: 16px;}.container { width: 100%;}.item { color: red;}CSSOM 树:body├── font-size: 16px└── .container ├── width: 100% └── .item └── color: redCSS 继承与层叠:
flowchart TB
A[样式声明] --> B{来源}
B -->|用户代理| C[优先级最低]
B -->|用户样式| D[优先级中等]
B -->|作者样式| E[优先级最高]
E --> F{重要性}
F -->|!important| G[最高优先级]
F -->|普通声明| H{特异性计算]
H --> I[ID 选择器: 100]
H --> J[类选择器: 10]
H --> K[元素选择器: 1]
2.5 执行 JavaScript
JavaScript 执行流程:
flowchart TB
A[下载 JS] --> B[解析]
B --> C[编译]
C --> D[执行]
D --> E{操作}
E -->|document.write| F[阻塞解析]
E -->|DOM API| G[修改 DOM]
E -->|CSSOM API| H[修改样式]
三、渲染层
3.1 构建渲染树
渲染树(Render Tree)是 DOM 和 CSSOM 的合并:
flowchart TB
subgraph 输入
A[DOM 树]
B[CSSOM 树]
end
subgraph 处理
C[合并] --> D[计算可见性]
D --> E[计算样式]
end
subgraph 输出
F[渲染树]
end
A --> C
B --> C
E --> F
渲染树特点:
- 不包含
display: none的元素 - 包含
visibility: hidden的元素 - 不包含
<head>等非可视元素
DOM 树: 渲染树:<html> <html> <head>...</head> <body> <body> <div> <div>可见</div> 可见 <div style="display:none"> <span>可见</span> 隐藏 </div> </div> </body> <span>可见</span> </html> </body></html>3.2 布局计算(Layout/Reflow)
布局计算确定每个元素的位置和大小:
flowchart TB
A[视口大小] --> B[根元素布局]
B --> C[块级元素]
C --> D[行内元素]
D --> E[计算位置]
E --> F[计算尺寸]
F --> G[写入布局树]
布局计算过程:
- 确定视口大小
- 从根元素开始递归
- 计算每个元素的盒子模型
- 处理浮动、定位、Flex/Grid
// 盒子模型计算const box = { content: { width: 100, height: 50 }, padding: { top: 10, right: 10, bottom: 10, left: 10 }, border: { top: 1, right: 1, bottom: 1, left: 1 }, margin: { top: 20, right: 0, bottom: 20, left: 0 },};
// 总宽度 = content + padding + border// 100 + 10*2 + 1*2 = 122px触发 Reflow 的操作:
| 操作 | 影响 |
|---|---|
| 窗口大小变化 | 全局 reflow |
| 字体变化 | 全局 reflow |
| 添加/删除元素 | 局部 reflow |
| 改变尺寸/位置 | 局部 reflow |
3.3 绘制(Paint)
绘制是将渲染树转换为像素的过程:
flowchart LR
A[渲染树] --> B[绘制列表]
B --> C[光栅化]
C --> D[位图]
D --> E[显示]
绘制顺序:
1. 背景色2. 背景图片3. 边框4. 子元素5. 轮廓触发 Repaint 的操作:
- 改变颜色
- 改变背景
- 改变 visibility
- 改变阴影
3.4 合成(Composite)
现代浏览器使用合成层优化渲染:
flowchart TB
subgraph 主线程
A[DOM/CSSOM] --> B[布局]
B --> C[绘制列表]
end
subgraph 合成线程
D[分块] --> E[光栅化]
E --> F[合成]
end
subgraph GPU
G[纹理上传] --> H[合成显示]
end
C --> D
F --> G
提升为合成层的条件:
| CSS 属性 | 效果 |
|---|---|
transform | 创建合成层 |
opacity | 创建合成层 |
will-change | 提示创建合成层 |
position: fixed | 可能创建合成层 |
/* 优化动画性能 */.animated-element { will-change: transform, opacity; transform: translateZ(0); /* 强制硬件加速 */}四、关键渲染路径优化
4.1 CRP 指标
timeline
title 关键渲染路径时间线
section 阻塞资源
DNS/TCP/TLS : 连接时间
HTML 下载 : 资源大小
CSS 下载 : 渲染阻塞
JS 执行 : 解析阻塞
section 渲染
DOM 构建 : 首次绘制
CSSOM : 首次内容绘制
布局/绘制 : 可交互时间
| 指标 | 英文 | 说明 |
|---|---|---|
| FP | First Paint | 首次绘制 |
| FCP | First Contentful Paint | 首次内容绘制 |
| LCP | Largest Contentful Paint | 最大内容绘制 |
| TTI | Time to Interactive | 可交互时间 |
| CLS | Cumulative Layout Shift | 累积布局偏移 |
4.2 优化策略
CSS 优化:
<!-- 内联关键 CSS --><head> <style> /* 首屏关键样式 */ .header { ... } .hero { ... } </style> <!-- 异步加载非关键 CSS --> <link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'" /></head>JavaScript 优化:
<!-- 关键 JS 内联 --><script> // 初始化代码</script>
<!-- 非关键 JS 延迟加载 --><script defer src="analytics.js"></script><script async src="ads.js"></script>
<!-- 动态加载 --><script> window.addEventListener("load", function () { var script = document.createElement("script"); script.src = "non-critical.js"; document.body.appendChild(script); });</script>资源预加载:
<!-- 预连接 --><link rel="preconnect" href="https://cdn.example.com" /><link rel="dns-prefetch" href="https://analytics.example.com" />
<!-- 预加载关键资源 --><link rel="preload" href="critical-font.woff2" as="font" crossorigin /><link rel="preload" href="hero-image.jpg" as="image" />
<!-- 预获取未来资源 --><link rel="prefetch" href="next-page.html" />五、浏览器架构
5.1 多进程架构
现代浏览器的多进程架构:
flowchart TB
subgraph Browser Process
A[主进程] --> B[UI 线程]
A --> C[网络线程]
A --> D[存储线程]
end
subgraph Renderer Processes
E[渲染进程 1]
F[渲染进程 2]
end
subgraph Utility Processes
G[GPU 进程]
H[插件进程]
I[扩展进程]
end
A <--> E
A <--> F
A <--> G
| 进程 | 职责 |
|---|---|
| Browser | UI、网络、存储、协调 |
| Renderer | 页面渲染、JavaScript 执行 |
| GPU | 图形渲染、合成 |
| Plugin | 插件运行 |
| Utility | 实用工具(如网络服务) |
5.2 渲染进程组成
flowchart TB
subgraph 渲染进程
A[主线程]
B[Worker 线程]
C[合成线程]
D[光栅线程]
end
A --> |HTML/CSS| E[DOM/CSSOM]
A --> |布局| F[Render Tree]
A --> |绘制| G[Paint Records]
G --> C
C --> D
D --> |位图| H[显示]
| 线程 | 职责 |
|---|---|
| Main | DOM、CSSOM、布局、绘制、JS |
| Worker | Web Worker、Service Worker |
| Compositor | 合成层管理、滚动处理 |
| Raster | 光栅化、位图生成 |
六、总结
完整流程图
flowchart TB
subgraph 用户操作
A[输入 URL]
end
subgraph 网络层
B[URL 解析] --> C[DNS 查询]
C --> D[TCP 三次握手]
D --> E[TLS 握手]
E --> F[HTTP 请求]
F --> G[接收响应]
end
subgraph 解析层
H[HTML 解析] --> I[DOM 树]
J[CSS 解析] --> K[CSSOM 树]
L[JS 执行]
end
subgraph 渲染层
M[渲染树] --> N[布局]
N --> O[绘制]
O --> P[合成]
P --> Q[显示]
end
A --> B
G --> H
G --> J
I --> M
K --> M
L -.-> I
关键要点
- 网络层:DNS、TCP、TLS、HTTP 是基础
- 解析层:DOM、CSSOM 构建可能被 JS 阻塞
- 渲染层:布局、绘制、合成是渲染三阶段
- 优化核心:减少关键资源、缩短 CRP 长度
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
浏览器渲染原理:从 URL 到页面
https://blog.souloss.com/posts/principles/browser-rendering-principles/ 部分信息可能已经过时
相关文章 智能推荐
1
HTTP/2:多路复用
web 探索 HTTP/2 如何突破 HTTP/1.1 的性能瓶颈——从 SPDY 的设计理念到二进制分帧层、多路复用、头部压缩(HPACK)、服务器推送与流优先级,用实验直观理解这些革命性改进。
2
HTTP/1.1:持久连接
web 在 HTTP/1.0 的基础上,深入探索 HTTP/1.1 的核心改进——持久连接、分块传输编码、请求管道化、内容协商、缓存增强,以及 Host 头的重要性。通过 Python 实验服务器亲手体验这些特性带来的性能提升。
3
HTTP/1.0:扩展协议
web 在 HTTP/0.9 的基础上,探索 HTTP/1.0 引入的核心新特性——请求头与响应头、状态码、多种 HTTP 方法(GET/POST/HEAD),并动手实现一个支持 HTTP/1.0 的 Python 服务器。
4
WebSocket 协议:双向通信
网络 深度解读 WebSocket 协议——握手流程、帧格式、心跳机制、代理穿透
5
DNS 解析完整流程:从域名到 IP
原理 深入剖析 DNS 解析的完整过程,从浏览器缓存到权威 DNS 服务器,涵盖递归查询、迭代查询、缓存策略、DNSSEC 等核心机制。






