浏览器渲染原理(Chrome)

58 阅读27分钟
flowchart TB
    %% 流程节点
    A([输入 URL]) --> B[DNS 解析]
    B --> C[TCP 三次握手建立连接]
    C --> D{是否 HTTPS}
    D --是--> E[TLS 握手协商加密]
    D --否--> F[跳过 TLS 直接请求]
    E & F --> G[发送 HTTP 请求]
    G --> H[服务器处理并返回响应]
    H --非持久--> I[TCP 四次挥手关闭连接]
    H --> J[开始渲染流程]
    
    %% 渲染子流程
    J --> K[构建 DOM 树]
    J --> L[构建 CSSOM 树]
    K & L --> M[合成渲染树]
    M --> N[布局计算]
    N --> O[绘制像素]
    O --> P[分层与合成显示]
    P --> Q[页面首次渲染完成]

    %% 资源与交互流程
    H --> R[并行加载子资源 JS/CSS/图片等]
    R --> S[JS 脚本解析与执行]
    S --> T[事件循环处理交互]
    T --> U[触发 DOM/CSS 变更]
    U --可能回流重绘--> V{需更新渲染}
    V --是--> N
    V --否--> W[保持当前状态]

输入 URL

当输入 URL 后,浏览器将对 URL 进行解析和预处理,主要分为两步:

  1. 区分输入类型: 对输入文本进行区分,纯文本调用搜索引擎,URL 则进行请求
  2. 补全协议与格式: 如果 URL 未包括协议(http://https://),浏览器默认优先使用https://协议进行补充

DNS 解析(域名解析)

浏览器获得完整 URL 后,接下来就是进行 DNS 解析。DNS 解析的目标就是将域名( example.com )转换为服务器 IP 地址。而域名解析的过程如下:

  1. 浏览器缓存查询: 浏览器自身维护一个 DNS 缓存,如果存在记录直接使用对应的 IP 地址
    1. 可通过chrome://net-internals/?#dns查看
  2. 系统缓存查询: 浏览器缓存中无记录,浏览器调用系统(Windows 或 macOS)的 DNS 缓存
    1. Windows 可通过ipconfig /displaydns查看
  3. DNS 服务器查询: 如果系统的 DSN 缓存中也没有记录,浏览器将会向本地 DNS 服务器(有运行商提供,如中国移动的 DNS)发送查询请求
    1. 本地 DNS 服务器将通过以下方式,最终获取到对应的 IP 地址TTL 时间
      1. 自身缓存
      2. 查询根域名服务器 → 顶级域名服务器(如.com) → 权威域名服务器(如example.com服务器)

DNS 缓存是有时间的,一般来说是根据 DNS 响应中的 TTL (生存时间)字段决定。

DNS 查询方式及详细过程

DNS 查询有两种查询方式:递归查询和迭代查询

递归查询:客户端发起查询请求,并且要求本地 DNS 服务器必须返回最终结果(失败或成功),本地 DNS 服务器将会负责查询的全程工作,客户端无需参与过程,只需要等待 DNS 服务器给出结果。

迭代查询:客户端或服务器在查询的时候,每次只能获取“下一步查询的指引”,需要逐步跳转至不同服务器查询,直到获取结果。与递归查询不同的是,每一步都需要主动方根据返回结果决定下一步操作,不会由单一服务器全程代理查询。

特性递归查询迭代查询
执行者由本地 DNS 服务器全程负责,替客户端查询客户端或服务器逐次向其他服务器查询,需自己处理中间结果
客户端参与程度仅发起一次请求,无需处理中间步骤需多次发送请求,或服务器需多次跳转查询
使用场景客户端向本地 DNS 服务器查询各级 DNS 服务器之间的交互

DNS 递归查询的详细过程,假设客户端访问 www.example.com

  1. 客户端将向本地 DNS 服务器发送查询请求,获取www.example.com的 IP 地址
  2. 本地 DNS 服务器检查自身缓存,如果有缓存将返回 IP 地址给客户端
  3. 本地 DNS 服务器没有缓存,将会向根 DNS 服务器发送查询请求,询问.com顶级域名服务器的 IP 地址
  4. 本地 DNS 服务器获取到根 DNS 服务器返回的顶级域名服务器的 IP 地址****
  5. 本地 DNS 服务器顶级域名服务器发送查询请求,询问example.com权威 DNS 服务器的 IP 地址
  6. 本地 DNS 服务器获取到顶级域名服务器返回的权威 DNS 服务器的 IP 地址
  7. 本地 DNS 服务器权威 DNS 服务器发送查询请求,询问www.example.com的 IP 地址
  8. 本地 DNS 服务器获取到权威 DNS 服务器返回的 DNS 响应,其中有解析到的 IP 地址和 TTL(生存时间 Time To Live)
  9. 本地 DNS 服务器根据 TTL 字段值设置缓存有效时间,并将 DNS 响应返回给客户端
sequenceDiagram
    participant C as 客户端
    participant L as 本地 DNS 服务器
    participant R as 根 DNS 服务器
    participant T as 顶级域名服务器(.com)
    participant A as 权威 DNS 服务器(example.com)

    %% 1. 客户端发起查询
    C->>L: 查询 www.example.com 的 IP 地址
    %% 2. 本地 DNS 检查缓存
    activate L
    L->>L: 检查自身缓存,无缓存则继续
    %% 3. 本地 DNS 向根服务器查询
    L->>R: 查询 .com 顶级域名服务器 IP
    activate R
    R->>L: 返回 .com 顶级域名服务器 IP
    deactivate R
    %% 4. 本地 DNS 向顶级域名服务器查询
    L->>T: 查询 example.com 权威 DNS 服务器 IP
    activate T
    T->>L: 返回 example.com 权威 DNS 服务器 IP
    deactivate T
    %% 5. 本地 DNS 向权威服务器查询
    L->>A: 查询 www.example.com 的 IP 地址
    activate A
    A->>L: 返回 IP 地址 + TTL
    deactivate A
    %% 6. 本地 DNS 缓存并返回客户端
    L->>L: 按 TTL 设置缓存有效期
    L->>C: 返回 www.example.com 的 IP 地址
    deactivate L

TCP 三次握手

经过 DNS 解析后,浏览器拿到服务器 IP 地址,将会通过 TCP 与服务器建立连接。

TCP 全称传输控制协议,TCP 协议属于传输层协议,其主要作用是将客户端应用层发送过来的数据分割成报文(最大传输单元),然后将报文传输给 IP 层(网络层),由它通过网络传送给服务器的 TCP 层

协议定义了数据如何在计算机内和计算机之间进行数据交换的规则。简单点来说就是定义了通信的数据格式。

TCP 的连接

  1. 第一次握手客户端行为:Syn=1 + Seq=A ,相当于大声喊「服务器你好,我想跟你建立连接!我后续发数据,会从序列号 A 开始编号哟~」
  2. 第二次握手服务器行为:会回复 Syn=1(自己也要发起连接同步 ) + Ack=1(确认收到客户端的 Syn 包 ) + Seq=B(服务器自己的初始序列号 ) + Ack Number=A+1(告诉客户端「你发的 Seq=A 我收到啦,下次你从 A+1 接着发」 )
  3. 第三次握手客户端行为:Ack=1 + Seq=A+1(基于之前的约定接着发 ) + Ack Number=B+1 ,三次握手完成,连接建立

TCP 通过三个消息来协商和启动 TCP 会话,三个消息分别是 Syn(同步)、Syn-Ack(同步-确认)、Ack(确认),通过三个消息在传输数据(HTTP 请求)之前协商连接的参数

sequenceDiagram
    participant C as 客户端
    participant S as 服务器

    %% 1. 客户端发送同步包
    C->>S: Flags: SYN=1<br>SEQ=A
		%% 2. 客户端状态改变
		C->>C: 状态:SYN_SENT
		%% 3. 服务器发送同步确认包
		S->>C: Flags: SYN=1 ACK=1<br>SEQ=B<br>ACK=A+1
		%% 4. 服务器状态改变
		S->>S: 状态:SYN_SENT
		%% 5. 客户端状态改变
		C->>C: 状态:ESTABLISHED
 		%% 6. 客户端发送确认包
    C->>S: Flags: ACK=1<br>SEQ=A+1<br>ACK=B+1
		%% 5. 服务器状态改变
		S->>S: 状态:ESTABLISHED
		
		Note over C, S: 三次握手完成<br>开始数据传输

第一次握手:客户端向服务器发送同步报文

客户端向服务器发送 Syn 包(同步报文),请求“建立连接”并告知服务器自己的序列号。客户端状态由 CLOSED 变为 SYN_SENT,表示已发送同步请求,等待服务器确认。Syn 包不携带实际数据,仅用于连接初始化。

Syn 包描述:

  1. 标志位Syn=1(请求“建立连接”)
  2. 序列号Seq=A(A 初始序列号,客户端发送的数据流起点)

TCP 第一次握手

第二次握手:服务器收到同步报文并发送同步确认报文

服务器收到客户端发送的 Syn 包后,会将该 Syn 包放入 Syn 队列(半连接队列,暂存未完成三次握手的连接),记录客户端信息**。并且向客户端回应一个 Syn-Ack(同步确认报文),服务器状态由 CLOSED 变为 SYN_RCVD,表示已收到请求,正在处理并等待客户端最终确认。如果服务器未收到后续 Ack 包,则会重传 Syn-Ack 包(默认重传 5 次)

Syn-Ack 包描述:

  1. 标志位Syn=1(服务器请求“建立连接”),Ack=1(确认客户端“连接请求”)
  2. 确认号Ack=A+1,表示已收到客户端序列号为 A 的 Syn 包,等待后续序列号从 A+1 开始的数据
  3. 序列号:Seq=B,服务器随机生成的初始序列号 B,标识服务器发送的数据流起点

TCP 第二次握手

第三次握手:客户端收到服务器同步确认报文并发送确认包

客户端收到服务器回应的 Syn-Ack 包后,将再向服务器发送一个 Ack 包,客户端状态变为 ESTABLISHEN,表示连接已建立。服务器接收到 Ack 包后,将连接从 Syn 队列移至 ACCEPT 队列

Ack 包描述:

  1. 标志位Ack=1(确认服务器“连接请求”)
  2. 确认号Ack=B+1,表示客户端收到服务器序列号为 B 的 Syn-Ack 包,等待服务器后续序列号从 B+1 开始的数据
  3. 序列号:Seq=A+1,表示客户端的下个数据字节将从 A+1 开始发送,与服务器回应的 Syn-Ack 包中 Ack=A+1 对应

TCP 第三次握手

服务器 TCP 握手中的两个队列

  1. Syn 队列:半连接队列,暂存未完成三次握手的连接。
  2. ACCEPT 队列:全连接队列,存放完成三次握手的链接。

标志位:SYN=1 表示建立连接同步序列号;ACK=1 表示同意建立连接并已同步序列号

确认号:ACK=A 表示已接收 A 之前的数据,请从 A 开始发送数据

序列号的重要作用:数据有序传输、丢包检测和重传、流量控制、连接唯一标识。

序列号递增过程:

  • 普通数据传输:Seq = preSeq + 载荷字节数(实际发送的数据长度);
  • 带 SYN/FIN 标志的包(无数据载荷):Seq = preSeq + 1(因控制位占 1 字节“虚拟载荷”)。

需要注意的是:TCP 会为每个字节的数据分配序列号,确保字节流有序;控制位(如 SYN/FIN)虽不承载数据,但会占用序列号计数。而且序列号有最高值,当达到最高值后,将会变为 0 重新递增。

TLS 协商

TLS 叫做传输层安全协议,是一种应用程序用来在网络中安全通信的协议,防止第三方窃听或篡改任何消息。

TLS 协议是可选的,当完成 TCP 协议后才会进行 TLS 协商,其协商过程也可以称之为 TCP 三次握手之后的“一次握手”。

客户端 <----   Syn   ----> 服务器  
客户端 <---- Syn+Ack ----> 服务器  
客户端 <----   Ack   ----> 服务器  
(TCP 连接就绪,开始 TLS 握手)

阶段 1:客户端发起握手(ClinetHello)

客户端告知服务器自身支持内容,包含 TLS 版本、生成客户端随机数 ClientRandom(32 字节随机数)、密码套件列表等信息。

ClinetHello

阶段 2:服务器响应并验证身份(ServerHello+证书+密钥交换)

服务端筛选双方兼容参数,验证身份并传递密钥协商关键内容。

  1. 从 ClientHello 中选择双方均支持的 TLS 版本和密码套件,生成服务器随机数 ServerRandom,组织成 ServerHello 发送给客户端。
  2. 向客户端发送证书,证书中包含了服务器公钥 ServerCertPubKey(后续验证服务器身份)、证书持有者的信息(域名、组织名称等)、CA 签名(证书颁发者)、有效期等信息。
  3. 基于算定的密码套件,结合 ClientHello、ServerHello 随机数还有其他关键参数,计算出服务器临时私钥 ServerECDHPrivKey 和公钥 ServerECDHPubKey 后,将公钥发送给客户端。

ServerHello

Certificate + ServerKeyExchange

步骤 3:客户端验证与密钥生成

客户端校验服务端身份,推导共享密钥并完成握手确认。

  1. 验证证书:验证是否在有效期内 → 通过客户端内置的根证书公钥验证 CA 签名 → 验证服务器域名是否和客户端访问域名匹配 → 验证证书是否被吊销。
  2. 通过证书中的服务器公钥 ServerCertPubKey 验证服务器的临时公钥 ServerECDHPubKey 是否来自服务器。
  3. 基于算定的密码套件,用 ClientHello、ServerHello 中的随机数还有其他关键参数,计算出客户端临时私钥 ClientECDHPrivKey 和公钥 ClientECDHPubKey。
  4. 基于 ECDH 算法,用客户端临时私钥 ClientECDHPrivKey+服务器临时公钥 ServerECDHPubKey 计算出共享秘密 DHSecret。
  5. 结合 ClientRandom、ServerRandom、DHSecret ,生成主密钥。
  6. 主密钥生成对称加密密钥,叫做派生会话密钥,用于后续加密通信。
  7. 用派生会话密钥,对握手消息(ClientHello+ServerHello+证书+密钥)进行加密,发送服务器验证密钥的正确性。

步骤 4:服务器生成密钥并完成确认

服务端推导共享密钥,验证客户端并完成握手闭环。

  1. 通过服务器临时私钥 ServerECDHPrivKey+客户端公钥 ClientECDHPubKey 计算出共享秘密,与客户端一致。
  2. 生成派生会话密钥(与客户端流程一致)。
  3. 解密并校验客户端发过来的 Finished 的是否匹配。
  4. 加密握手消息,发送客户端进行验证。

步骤 5:开始安全通信

客户端和服务器切换至应用层协议用协商好的派生会话密钥加密传输数据。


sequenceDiagram
    participant C as 客户端
    participant S as 服务器

    %% 1. 客户端发起ClientHello
    C->>S: ClientHello<br>[TLS版本、ClientRandom、密码套件、ClientECDHPubKey(客户端临时公钥)、SNI等扩展]
    
    %% 2. 服务器回应ServerHello+证书
    S->>C: ServerHello<br>[选定TLS版本、ServerRandom、密码套件、ServerECDHPubKey(服务器临时公钥)]
    S->>C: Certificate<br>[服务器证书链(验证身份)]
    
    %% 3. 客户端:验证证书+派生密钥+生成Finished
    C->>C: 验证证书(CA信任、域名、有效期)
    C->>C: ECDHE交换→预主密钥→主密钥→会话密钥
    C->>C: 生成Finished(哈希握手消息+会话密钥加密)
    
    %% 4. 客户端发Finished,确认密钥
    C->>S: Finished
    
    %% 5. 服务器:派生密钥+验证客户端Finished+生成自身Finished
    S->>S: 同客户端派生会话密钥
    S->>S: 验证客户端Finished(解密后哈希对比握手消息)
    S->>S: 生成服务器Finished(哈希握手消息+会话密钥加密)
    
    %% 6. 服务器发Finished,双向确认
    S->>C: Finished
    
    %% 7. 客户端验证服务器Finished
    C->>C: 验证服务器Finished(解密后哈希对比)
    
    Note over C, S: TLS 1.3 协商完成<br>开始加密数据传输

HTTP 请求

经过 TCP 握手(如果是 HTTPS 协议还有 TLS 协商),浏览器将构建请求报文,分为三部分组成:请求行、请求头、请求体。

  1. 请求行:请求方法、资源路径、HTTP 版本
  2. 请求头:控制缓存、客户端能力告知、连接状态、认证信息和安全、请求体的媒体类型
  3. 请求体:媒体类型对应的请求体数据

HTTP 协议版本区别

  1. 1.0版本:1996 年发布,默认短连接,引入了请求头和响应头的概念
  2. 1.1版本:1997 年发布,目前使用最多的版本,其主要特性:
    1. 默认开启长连接(Connection: keep-alive),允许一个 TCP 连接上发送多个请求和响应
    2. 连接支持管道化,基于长连接的特性,客户端在一个连接上发送多个请求,无需等待服务器响应,但是服务器要按照请求顺序按序返回
    3. 完善缓存机制,增加Cache-Control等头字段。
    4. 允许字节范围请求(断点续传功能的基础),允许请求资源的某一部分数据。
  3. 2.0版本:2015 年发布,其主要特点是:多路复用、服务器推送、二进制分帧、头部压缩。
    1. 二进制分帧:将数据体和头信息用二进制形式传输,将通信单位缩小为帧,相较于1.1版本的文本形式传输,解析效率更高,而且二进制形式数据更紧凑
    2. 多路复用:一个 HTTP 连接可以并行发送和响应多个请求,将请求和响应分解为多个独立且带有标识的流,不同流之间的数据可以同时传输,避免了1.1版本的队头阻塞问题
    3. 头部压缩:采用算法压缩头部;客户端和服务器维护索引表,记录中出现过的头部,在后续传输中直接引用键名,服务器接收到头部键名,通过索引表找出真正的值,避免重复发送节省带宽。
    4. 服务器推送:服务器可以主动向客户端发送资源,无需客户端请求。
  4. 3.0 版本:2018 年发布,放弃 TCP,使用基于 UDP 的 QUIC 协议,默认使用 TLS 加密。解决2.0版本中 TCP 的队头阻塞问题,同时无需 TCP 复杂握手过程。

队头阻塞:

1.1 版本虽然支持长连接和管道化,但是本质还是按序处理请求,当一个请求响应较慢,会导致后续请求的响应被阻塞。

TCP 的滑动窗口机制有序传输特性是导致阻塞的核心原因:

  1. 滑动窗口机制:TCP 报文中携带的 Window 字段的值,表示的是接收方当前可用缓存空间(字节)。发送方会根据这个空间大小动态调节发送数据速率,防止接收方缓存溢出。如果接收方处理速度慢,就造成了阻塞。
  2. 有序传输特性:TCP 会为每个字节添加序列号,如果中间丢包,后续数据就算到达接收方,也需要等待丢失数据重传成功,才会一起按序交给应用层。

常用请求方法和重要概念

HTTP 常用方法核心作用 & 特性幂等性
GET通过 URL 传递参数获取资源,长度受浏览器 / 服务器限制;结果可被浏览器、CDN 缓存🟢多次请求同资源,结果一致
POST向服务器提交数据(表单、文件等),用于创建新资源;默认不缓存请求体数据🔴多次提交可能重复创建
PUT全量更新资源,用请求体数据完整替换目标资源内容;需传递全量数据(若资源不存在,部分服务会创建)🟢多次全量替换,最终状态一致
DELETE删除指定资源(需服务端校验权限 / 逻辑)🟢多次删除,最终状态一致
PATCH对目标资源部分更新,仅修改请求体中提供的字段;更灵活但需服务端适配逻辑🔴多次执行可能叠加修改
HEAD类似 GET,但仅返回响应头(状态码、元数据等),不返回响应体;用于预检资源状态🟢同 GET,仅查元数据
OPTIONS浏览器跨域预检专用,自动发送;请求服务器返回支持的请求方法、Headers 等权限信息🟢多次预检结果一致

请求方法的幂等性

幂等性概念是数学中的概念,若一个操作重复多次执行,其结果与执行一次相同,则称该操作具有幂等性。延伸到接口设计角度,一个请求被重复提交时,对服务器资源的最终影响与执行一次相同且不会产生数据重复的副作用,则该请求具有幂等性(如 GET 请求)。

幂等方法:GET、HEAD、PUT、DELETE、OPTIONS

  • PUT 请求:虽然改变了资源状态状态,但是_第一次改变资源状态后,再发送相同请求,资源还是第一次改变后的状态_。
  • DELEATE 请求:删除指定资源后,资源状态变为“不存在”,再次请求删除资源,资源状态还是“不存在”。
  • OPTIONS 请求:查询服务器某资源支持的请求方法,多次请求也不会改变。

非幂等方法:POST、PATCH

  • POST 请求:用于创建资源或者触发有副作用的操作。如果后台不进行特殊处理,每次请求都会创建一个新资源,哪怕数据相同。
  • PATCH 请求:理论上来讲,后台会对请求进行逻辑约束,PATCH 应用场景是针对某个字段值进行覆盖操作,所以看起来是幂等操作;但是因为其灵活性,通常归为非幂等

简单请求和复杂请求

简单请求和复杂请求的划分源于浏览器的同源策略跨域机制,两者的核心区别是浏览器是否触发浏览器自动发起预检请求,如果触发则为复杂请求

浏览器同源策略

浏览器的同源策略会阻止跨域的非简单请求与服务器直接交互,必须发送预检请求,经过服务器允许后才会发送实际请求。但是简单请求,浏览器直接放行。

简单请求需满足以下条件:

  1. 请求方法:POSTGETHEAD
  2. 请求头不包含自定义请求头(如不包含 X-Custom-Header 等开发者自定义字段),只有AcceptAccept-LanguageContent-LanguageContent-Type(仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain 三种格式)

如何避免不必要的预检?

  1. 优先使用简单请求格式,若无需复杂方法或自定义头,尽量用 POST + form 表单格式,避免触发预检。
  2. 减少不必要的自定义头字段,避免因单个头字段触发预检
  3. 合理设置 Access-Control-Max-Age:缓存预检结果,减少重复预检的网络开销

TCP 四次挥手

TCP 的四次挥手是终止连接的过程,一般来说 HTTP/1.1 版本默认启用长连接(请求头 Connection: keep-alive ),也就是说服务器响应请求后,TCP 并不会马上关闭,而是等到连接一定时间内没有使用后,由客户端或者服务器主动关闭。

客户端和服务器对于连接空闲时间都有一个阈值,到达阈值将会关闭连接

TCP 连接终止:

  1. 第一次挥手客户端行为:Fin=1 + Seq=C ,相当于客户端大声喊「服务器,我这边数据都发完啦,准备关闭连接咯!我这次发的数据包编号是 C ,你留意下~」喊完后,客户端进入 FIN-WAIT-1(终止等待 1)状态,静静等着服务器回应。
  2. 第二次挥手服务器行为:收到客户端的 “告别信号” 后,服务器立马回复 Ack=1 + Seq=D + Ack Number=C+1 ,就像回应说「客户端兄弟,你编号 C 的数据包我收到啦!知道你要关连接了,我这边先记着,等我处理完手头数据就准备关闭 」。发完这条消息,服务器进入 CLOSE-WAIT(关闭等待)状态,开始处理剩余数据。而客户端收到回复后,就切换到 FIN-WAIT-2(终止等待 2)状态,耐心等着服务器处理完。
  3. 第三次挥手服务器行为:当服务器把剩余数据都发送完毕,它发送 Fin=1 + Ack=1 + Seq=E + Ack Number=C+1 ,意思是「客户端,我这边数据也处理完了,可以正式关闭连接啦!我这次数据包编号是 E ,你确认下 」。发完后,服务器进入 LAST-ACK(最后确认)状态,忐忑地等着客户端最后的回应。
  4. 第四次挥手客户端行为:客户端收到服务器的关闭请求后,回复 Ack=1 + Seq=C+1 + Ack Number=E+1 ,相当于是说「服务器,我收到你的关闭请求啦!编号 E 的数据包没问题,咱们这就断开连接 」。客户端发送完这条确认消息,进入 TIME-WAIT(时间等待)状态,在这个状态停留 2 倍的 MSL(最长报文段寿命)时长,确保没有丢包或异常情况。而服务器收到确认后,就直接进入 CLOSED(关闭)状态,彻底断开连接。客户端在 TIME-WAIT 状态等待结束后,也进入 CLOSED 状态,至此,TCP 连接完全关闭。
sequenceDiagram
    participant C as 客户端
    participant S as 服务器

    %% 1. 客户端请求关闭连接
    C->>S: Flags: FIN=1<br>SEQ=C
    C->>C: 状态:FIN-WAIT-1

    %% 2. 服务器确认关闭请求
    S->>C: Flags: ACK=1<br>SEQ=D<br>ACK=C+1
    S->>S: 状态:CLOSE-WAIT
    C->>C: 状态:FIN-WAIT-2

    %% 3. 服务器请求关闭连接
    S->>C: Flags: FIN=1 ACK=1<br>SEQ=E<br>ACK=C+1
    S->>S: 状态:LAST-ACK

    %% 4. 客户端确认关闭请求
    C->>S: Flags: ACK=1<br>SEQ=C+1<br>ACK=E+1
    C->>C: 状态:TIME-WAIT
    S->>S: 状态:CLOSED

    %% 5. 客户端等待2MSL后关闭
    C->>C: 等待2MSL
    C->>C: 状态:CLOSED

    Note over C, S: 四次挥手完成<br>TCP连接彻底关闭

第一次挥手:客户端请求关闭连接

客户端向服务器发送FIN 包(结束报文),请求关闭连接并告知服务器自己已经完成数据发送。客户端状态由 ESTABLISHED(已建立)变为 FIN-WAIT-1(终止等待 1),表示已发送“关闭请求”,等待服务器确认。FIN 包可携带最后一批数据,但是一般为空。

Fin 包描述:

  1. 标志位:Fin=1(请求“关闭连接”)
  2. 序列号:Seq=C(C 为客户端当前发送序号,标识最后一个数据字节+1)

第二次挥手:服务器确认关闭请求

服务器收到客户端的 Fin 包后,立即发送 Ack 包(确认报文),表示已收到请求。服务器状态由 ESTABLISHED 变为 CLOSE-WAIT(关闭等待,也叫半关闭状态),表示同意关闭连接,但是需要先处理剩余数据。此时客户端到服务器的单项链接已关闭(客户端不再发送数据),但是服务器可以向客户端发数据

Ack 包描述:

  1. 标志位:AcK=1(确认收到 Fin 包)
  2. 确认号:Ack=C+1(确认收到客户端序号 C 及之前的所有数据)
  3. 序列号:Seq=D(D 为服务器当前发送序号,标识剩余数据起点)

第三次挥手:服务器请求关闭连接

服务器处理完剩余数据后,向客户端发送 Fin 包,表示自己也已完成数据发送,请求关闭连接。服务器状态由 CLOSE-WAIT 变为 LAST-ACK(最后确认),表示等待客户端的最终确认。

Fin 包描述:

  1. 标志位:Fin=1(请求“关闭连接”),Ack=1(确认客户端的 Fin 请求)
  2. 序列号:Seq=E(E 为服务器最后一个数据字节 + 1)
  3. 确认号:Ack=C+1(与第二次挥手相同,确认客户端的 FIN)

第四次挥手:客户端确认关闭请求

客户端收到服务器的 Fin 包后,发送 ACK 包,表示已收到关闭请求。客户端状态由 FIN-WAIT-2(终止等待 2)变为 TIME-WAIT(时间等待),并等待 2MSL(最大段生存时间) 以确保服务器收到确认。服务器收到 ACK 后,状态由 LAST-ACK 变为 CLOSED(已关闭)客户端在 2MSL 后也进入 CLOSED 状态,连接彻底终止

Ack 包描述:

  1. 标志位:ACK=1(确认收到 Fin)
  2. 确认号:Ack=E+1(确认收到服务器序号 E 及之前的所有数据)
  3. 序列号:Seq=C+1(与第一次挥手中的 Fin 包序号一致)

四次挥手与三次握手的核心差异

  1. 挥手多一次:关闭连接时,服务器需先确认客户端的 Fin(第二次挥手),再单独发送自己的 fin(第三次挥手),因此共四次。
  2. TIME-WAIT 状态:仅客户端需要等待 2MSL,确保网络中残留的数据包不会影响新连接。
  3. 半关闭状态:第二次挥手后,服务器仍可向客户端发送数据,形成 “半关闭” 状态。

关键渲染路径

构建 DOM 树

当浏览器通过网络(或本地缓存)接收到服务器返回的 HTML 数据时,会立即启动解析器(HTML Parser)开始工作。整个解析过程以 “流式处理” 的方式逐字节接收、逐阶段处理,最终将字节流转换为 DOM 树(Document Object Model Tree)。为了加快渲染过程,浏览器还启动了预加载扫描器与解析器配合。当解析器阻塞时,预加载扫描器将继续识别网络资源进行下载。

字节流输入 → 词法分析(Token 生成) → 语法分析(节点生成) → 树结构构建(节点层级关联) → DOM 树完成
  1. 字节流转字符流
  2. 词法分析器将字符流拆成 Token(标签类、属性类、文本类、特殊符号),每个 Token 包含类型和内容
  3. 语法分析器将 Token 转为节点,并确定节点之间的层级关系(可以看 Vue 的模板解析代码实现)
  4. 将独立的 DOM 节点按照语法分析确定父子关系,组成 DOM 树

流式处理(Stream Processing) 的核心特点就是 边接收数据边处理,无需等待整个数据传输完成。

浏览器收到服务器返回的 第一个 HTML 字节 时,解析器会立即开始工作,浏览器通过网络模块(如 TCP 层)分段接收数据,解析器则对已接收的字节流按顺序处理,两者同步进行。

主解析器阻塞场景:

  1. JS 可能修改 DOM → 必须阻塞解析以等待 JS 执行完毕。
  2. JS 依赖 CSSOM → 必须先等待 CSS 解析 → 间接阻塞 HTML 解析。

当解析器阻塞(语法分析器阻塞,DOM 树构建暂停) 时,预加载扫描器不受影响,继续扫描已接收但未解析的标记字符串。字节流解码(编译器)、词法分析(Token 生成)通常不阻塞,为预加载扫描器提供持续的字符流输入。

“阻塞” 特指主解析器暂停 DOM 树构建(语法分析器暂停),但词法分析器可能继续工作(如 Chrome 会预解析后续字节流生成 Token 缓存)。

预加载扫描器

预加载扫描器是浏览器优化页面渲染速度的重要组件,与主 HTML 解析器协同工作。

当主 HTML 解析器开始处理 HTML 数据时,预加载扫描器同步启动,基于主解析器通过编码检测(如 UTF-8)解码为字符流(标记字符串)或词法分析生成的 Token 序列 进行扫描。识别 HTML 标记中的网络资源引用(如 <img src><link href><script src> 等),提取 URL 并触发浏览器网络进程提前下载资源。

预加载扫描器只能识别 HTML 原始标记中的资源(如服务器端渲染的静态标签),无法扫描动态注入内容(如 JS 通过 innerHTML 添加的 <img> )。

虽然看上去预加载扫描器和解析器是并行,但是实际上是交替。当 HTML 解析器遇到资源标签时将暂停解析并通知预加载扫描器对资源进行快速扫描,扫描完成后 HTML 解析器继续执行。

构建 CSSOM 树

构建 CSSOM 树的过程与 DOM 树的构建过程类似,均需经历字节流解码、词法分析与语法树生成阶段。

CSS 资源来源主要有四类:外部样式表内部样式内联样式,以及 JS 脚本动态注入 的样式。

浏览器解析 HTML 时,若遇到样式资源,会交由 CSS 解析器处理。其中,外部 CSS 资源先由预加载扫描器提交给网络进程下载,下载完成后再进入解析流程;内部和内联样式则直接进行解析。CSS 样式的解析过程默认不会阻塞 DOM 树的构建,两者并行执行。但当 JS 脚本执行时,如果涉及读取计算样式(如 getComputedStyle )或动态修改样式,需等待 CSSOM 构建完成,此时会间接阻塞 DOM 树的解析

值得注意的是,虽然 CSSOM 构建不会直接阻塞 DOM 树生成,但它是渲染树构建的必要前置条件。渲染树的构建依赖于完整的 DOM 树和 CSSOM 树,只有二者均构建完毕,浏览器才能进行样式计算、布局与绘制。

渲染树构建、布局、分层、绘制、合成

建渲染树:确定要显示的元素和样式 → 2. 算位置大小:布局确定元素在哪里 → 3. 分层管理:把元素按层级分组 → 4. 画图层内容:生成每个图层的像素数据 → 5. 合并图层:叠出最终显示的画面。

渲染树构建

确定页面上哪些元素需要显示,以及它们的样式(颜色、大小、位置等)。

  • 浏览器先解析 HTML 生成 DOM 树(描述页面结构),再解析 CSS 生成 CSSOM 树(描述样式)。
  • 将两棵树合并成渲染树(Render Tree),只包含页面中可见的元素(如 <div><p> 等,隐藏的元素如 display: none 会被排除)。

布局(Layout)

计算每个元素在页面中的具体位置和尺寸

  • 渲染树中的元素默认按**文档流(Normal Flow)**排列(从上到下、从左到右)。
  • 浏览器根据元素的样式(如widthheightmarginposition等),计算出每个元素在页面上的坐标(x/y 位置)占据的空间大小
  • 这一步也叫回流(Reflow),当元素尺寸或位置变化时(如动态修改样式),会重新触发布局。

分层(Layerization)

将页面元素按层级关系分组,优化后续绘制和合成效率。

  • 浏览器自动将需要独立处理的元素(如透明度、3D 变换、复杂动画的元素)分到不同的**图层(Layer)**中。
  • 例如视频播放器、浮动元素(position: fixed)、使用 opacity 的元素通常会被分到单独的图层。
  • 分层后,每个图层可以独立绘制,避免不同层级元素之间的干扰。

绘制(Painting)

将每个图层的元素像素化,生成「绘制指令」。

  • 针对每个图层,浏览器按照元素的样式(如背景色、边框、文字颜色等),逐像素 计算出该图层的视觉效果。
  • 这一步会生成绘制列表(Paint Record),记录每个元素需要绘制的内容和顺序(如先画背景,再画边框,最后画文字)。

合成(Compositing)

将所有图层的绘制结果合并成最终的屏幕图像

  • 浏览器将各个图层的位图(Bitmap)按照层级关系(z-index)进行叠加,最终生成完整的页面图像。
  • 合成操作在合成线程(Compositor Thread)中完成,支持硬件加速(GPU 加速),因此动画、滚动等操作通常更流畅。
  • 由于合成只需要移动或合并图层的位图,而无需重新布局或绘制,所以性能较高。