在浏览器里,从输入 URL 到页面展示,这中间发生了什么?
"在浏览器里,从输入 URL 到页面展示,这中间发生了什么?" 这是一道经典的面试题,能够全面考察应聘者对网络、操作系统、Web 等一系列知识的掌握程度。今天,我将结合下图对这个过程进行分析。当然,实际过程比图示更加复杂。
一、用户输入
用户在地址栏输入查询关键字后,浏览器会判断输入的是搜索内容还是请求的 URL。
如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,合成带关键字的 URL。如果输入内容符合 URL 规则,浏览器会根据规则加上协议,合成完整的 URL。
用户输入关键字并按下回车后,当前页面将被替换为新页面。在此之前,浏览器会触发当前页面的 beforeunload
事件,允许页面执行数据清理操作或询问用户是否离开页面,例如处理未提交的表单等。如果当前页面未监听 beforeunload
事件或用户同意继续,浏览器进入加载状态。
简而言之,浏览器处理完 URL 后,会将 URL 请求发送给网络进程。
二、网络请求
接下来进入页面资源请求过程。浏览器进程通过进程间通信(IPC)将 URL 请求发送至网络进程,网络进程接收到请求后,开始真正的 URL 请求流程。
首先,网络进程会检查本地缓存是否有资源。如果存在缓存资源:
1. 浏览器缓存
涉及强缓存和协商缓存两个概念。
服务端缓存控制
强缓存与服务端缓存控制相关,启用强缓存的情况包括:
存在 Cache-Control
属性,设置 max-age
并且不存在 no-cache
和 no-store
;
不存在 Cache-Control
,存在 Expires
字段。
服务端缓存控制状态:
no-store
:不允许缓存,适用于变化频繁的数据,如秒杀页面。no-cache
:允许缓存,但使用前必须向服务器验证是否过期或有新版本。must-revalidate
:缓存未过期时可使用,过期后需向服务器验证。
与缓存代理相关的属性:
private
:缓存仅限客户端,不能通过代理服务器缓存。public
:缓存完全开放,代理服务器可缓存和使用。
客户端缓存控制
进入协商缓存前,需要经过 DNS 解析、建立 TCP/IP 连接,以及如果是 HTTPS 协议,还需建立 TLS 连接。
协商缓存由客户端发起(条件请求),如果未命中强缓存,则进入协商缓存阶段。条件请求常用的头字段有 If-Modified-Since
和 If-None-Match
。这些字段需要第一次响应提供 Last-Modified
和 ETag
,以验证资源是否最新。
如果资源未变化,服务器回应 304 Not Modified
,表示缓存有效。
请求过程中可能遇到服务器返回状态码 301
或 302
。
重定向
重定向由服务器发起,浏览器接收到 301
或 302
状态码后,会跳转到新的 URI。重定向需配合 Location
字段使用。
Connection: keep-alive
Content-Length: 151
Content-Type: text/html
Date: Sun, 21 Feb 2021 00:48:52 GMT
Location: /index.html
Referer: /18-1
Server: openresty/1.19.3.1
Location
字段指定新的 URI,浏览器根据此字段发起新的 HTTP 或 HTTPS 请求。URI 可为绝对或相对路径,站内跳转可用相对 URI,站外需用绝对 URI。重定向报文还可使用 Refresh
字段实现延时重定向。
2. DNS 解析
若本地缓存不存在资源,需判断是否需要 DNS 解析。若 URI 是域名,则进行 DNS 解析获取服务器 IP 地址。
DNS 系统
DNS 系统为三层树状分布式服务,对应域名结构:
- 根域名服务器(Root DNS Server):管理顶级域名服务器,返回顶级域名服务器的 IP 地址。
- 顶级域名服务器(Top-level DNS Server):管理特定域名下的权威域名服务器,返回域名服务器的 IP 地址。
- 权威域名服务器(Authoritative DNS Server):管理域名下主机的 IP 地址。
通过树形结构查询域名,从右到左顺序查找,最终获得 IP 地址。核心系统外,DNS 解析还依赖两种缓存手段:
非权威域名服务器:大公司或网络运营商建立的 DNS 服务器,代替用户访问核心 DNS 系统。
操作系统缓存:操作系统缓存 DNS 解析结果,并查找主机映射文件(如 Linux 的 /etc/hosts
,Windows 的 C:\WINDOWS\system32\drivers\etc\hosts
)。
浏览器也有自己的 DNS 缓存,缓存策略依赖浏览器。
DNS 解析流程
DNS 查询流程为:
浏览器缓存 → 操作系统缓存 → 本地 hosts 文件 → 非权威域名服务器 → 根域名服务器 → 顶级域名服务器 → 权威域名服务器。
在 Windows 10 中,启动时会缓存 hosts 文件条目,并动态更新操作系统缓存。
3. 建立 TCP/IP 连接
获取服务器 IP 后,建立 TCP/IP 连接。
网络分层模型
TCP/IP 协议分为四层:
链接层(Link Layer):在以太网、WiFi 等底层数据上发送原始数据包,使用 MAC 地址标记设备。 网际层(Internet Layer):IP 协议所在层。 传输层(Transport Layer):保证数据在两点间可靠传输,TCP 和 UDP 协议位于此层。 应用层(Application Layer):面向具体应用的协议,如 HTTP。
三次握手
了解以下概念:
- ACK:确认字符。
- SYN:同步序列编号。
主要状态:
- LISTEN:监听 TCP 端口的连接请求。
- SYN-SENT:发送连接请求后等待确认。
- SYN-RECEIVED:收到连接请求后等待确认。
- ESTABLISHED:连接已建立,可发送数据。
三次握手流程:
- 客户端发送 SYN 标志位(序号 J),进入 SYN-SENT 状态。
- 服务器收到 SYN J,发送 ACK(序号 J+1)和 SYN(序号 K),进入 SYN-RECEIVED 状态。
- 客户端发送 ACK(序号 K+1),服务器收到后进入 ESTABLISHED 状态,连接建立。
序号 J 和 K 是初始序列号(ISN)。SYN 报文不携带数据,占用一个序号。客户端随机选择 ISN。
ACK 确认 SYN 段已接收,指定下次发送段的序号。建立连接后,浏览器立即发送请求报文。
4. 建立 TLS 连接
建立 TCP/IP 连接后,如果协议是 HTTPS,还需建立 TLS 连接。
HTTPS
HTTP 不安全,HTTPS 增加了机密性、完整性、身份认证和不可否认性。它将 HTTP 的传输协议由 TCP/IP 换为 SSL/TLS,即 "HTTP over SSL/TLS"。
SSL/TLS
SSL(Secure Sockets Layer)由网景公司于 1994 年发明,发展到 v3 时被 IETF 改名为 TLS(Transport Layer Security),标准化为 TLS1.0。
当前主流是 TLS1.2,包含记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等,综合使用对称加密、非对称加密、身份认证等技术。
对称加密、非对称加密
对称加密使用相同密钥加密和解密,如 AES 和 ChaCha20。AES 是 DES 的替代,ChaCha20 性能优于 AES。
非对称加密使用公钥和私钥,如 RSA 和 ECC。公钥可公开,私钥需保密。公钥加密只能用私钥解密,解决密钥交换问题。
混合加密
混合加密结合对称加密和非对称加密。初始通信使用非对称加密解决密钥交换,随后使用对称加密进行高效通信。
数字签名与证书
摘要算法(如 SHA-2)保证数据完整性。数字签名结合私钥和摘要算法,实现身份认证和不可否认性。
数字证书由 CA 颁发,包含公钥和身份信息,构建信任链。浏览器和操作系统内置根证书,通过验证证书链确认公钥可信。
ECDHE 握手过程
TLS 握手过程使用混合加密、摘要算法、数字签名和数字证书。ECDHE 握手流程如下:
- 客户端发送 "Client Hello" 消息,包含版本号、支持的密码套件和随机数。
- 服务器回复 "Server Hello",选择密码套件,发送证书和 "Server Key Exchange" 消息,包含椭圆曲线公钥和签名,最后发送 "Server Hello Done"。
- 客户端验证证书,发送 "Client Key Exchange" 消息,包含客户端公钥参数。
- 双方使用 ECDHE 算法生成 "Pre-Master Secret",结合随机数通过 PRF 生成 "Master Secret"。
- 客户端发送 "Change Cipher Spec" 和 "Finished" 消息,服务器验证后也发送相同消息。
- 握手结束,开始加密通信。
三、处理响应数据
HTTP 请求的数据类型通过 Content-Type
头字段区分。Content-Type
指示响应体的数据类型,浏览器根据其值决定如何处理。
如果 Content-Type
被判断为下载类型,浏览器会交由下载管理器处理,导航流程结束。如果是 HTML,浏览器继续导航流程,准备渲染进程。
四、准备渲染进程
默认情况下,Chrome 为每个页面分配一个渲染进程。若多个页面属于同一站点,则复用渲染进程。
同一站点定义为相同根域名(如 geekbang.org)、协议(https:// 或 http://)、子域名和端口。例如:
https://time.geekbang.org
https://www.geekbang.org
https://www.geekbang.org:8080
Chrome 的策略:
- 通常为每个新页面创建独立渲染进程。
- 若新页面与父页面同属一站点,复用父页面的渲染进程。
渲染进程准备好后,浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,进入提交文档阶段。
五、提交文档
提交文档指浏览器进程将网络进程的 HTML 数据提交给渲染进程,流程如下:
- 浏览器进程接收到响应头后,向渲染进程发送“提交文档”消息。
- 渲染进程与网络进程建立数据传输管道。
- 文档数据传输完成后,渲染进程返回“确认提交”消息。
- 浏览器进程收到确认消息,更新界面状态,包括安全状态、地址栏 URL、历史记录等。
完成导航流程后,进入渲染阶段。
六、渲染阶段
渲染模块执行多个子阶段,将 HTML 转换为像素,形成渲染流水线。主要子阶段包括构建 DOM 树、样式计算、布局、分层、绘制、分块、光栅化和合成。
1. 构建 DOM 树
浏览器将 HTML 转换为 DOM 树,表示页面的结构。
2. 样式计算
计算 DOM 树中每个元素的具体样式,分为以下步骤:
转换 CSS 为浏览器理解的结构
CSS 样式来源:
- 通过
link
引用的外部 CSS 文件。 - 元素的
style
属性内嵌的 CSS。
浏览器将 CSS 文本转换为 styleSheets
结构,可在 Chrome 控制台中查看 document.styleSheets
。
标准化样式属性值
将 CSS 属性值转换为渲染引擎理解的标准化值,如将 2em
、blue
、bold
转换为具体数值。
计算具体样式
根据 CSS 继承和层叠规则,计算 DOM 树中每个节点的最终样式。例如:
body {
font-size: 20px;
}
p {
color: blue;
}
span {
display: none;
}
div {
font-weight: bold;
color: red;
}
div p {
color: green;
}
对应的 DOM 树样式:
子节点继承父节点样式,如 body
的 font-size
为 20px
,其下所有节点的 font-size
亦为 20px
。
3. 布局阶段
计算 DOM 树中可见元素的几何位置,包含创建布局树和布局计算。
创建布局树
从 DOM 树中筛选可见节点,构建布局树。
不可见节点(如 head
标签、display: none
元素)不包含在布局树中。
布局计算
计算布局树节点的坐标位置,渲染引擎将布局运算结果写回布局树。Chrome 正在重构布局系统,以更清晰地分离输入和输出。
4. 分层
为特定节点生成图层,形成图层树(LayerTree)。
图层叠加形成最终页面图像:
布局树节点可能不对应独立图层,最终每个节点从属于某一图层。
创建图层的条件:
拥有层叠上下文属性的元素,如定位属性、透明属性、CSS 滤镜等。
需要剪裁的元素也会创建新图层。
七、绘制和光栅化
1. 绘制
绘制阶段会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
例如,一个图层包含文字和阴影,那么绘制过程会被拆分为:
- 绘制阴影
- 绘制文字
- 绘制边框
这些指令都会被记录在绘制列表中。
2. 光栅化
光栅化是将绘制指令转换为屏幕上的像素点的过程。
由于一个页面通常很大,但用户只能看到其中的一部分,所以合成线程会将图层划分为图块(tile),然后优先光栅化视口附近的图块。
光栅化过程主要包括:
- 图块划分:将图层分成多个小的图块
- 优先处理:优先处理视口附近的图块
- GPU 加速:使用 GPU 进行光栅化,提高处理速度
最佳实践:
- 使用
will-change
属性提示浏览器哪些元素可能发生变化,让浏览器提前做好优化准备 - 合理使用图层,过多的图层会增加内存占用和合成时间
- 避免大面积重绘,可以通过分层来减少重绘范围
八、合成
合成是将光栅化后的图层合并成最终页面的过程。主要包含以下步骤:
1. 图层合成
合成线程会按照图层的前后顺序,将所有图层叠加在一起。这个过程考虑:
- 图层的层叠顺序(z-index)
- 透明度(opacity)
- 变形(transform)等属性
2. 显示合成结果
合成线程将最终的合成结果提交给 GPU 进程,由 GPU 进行最后的渲染,显示在屏幕上。
这个过程的特点是:
- 在合成线程中进行,不会阻塞主线程
- 可以利用 GPU 硬件加速
- 不需要重新计算布局和绘制
最佳实践:
- 使用 CSS transform 进行动画,可以避免重排和重绘
- 合理使用 will-change 属性,提示浏览器提前创建独立图层
- 控制图层数量,避免过多图层导致内存占用过大
- 使用 requestAnimationFrame 来执行动画,保证动画流畅