想搞懂浏览器是怎么变魔术,把代码变成眼前漂亮页面的吗?🎩✨ 这次我们就以 www.baidu.com/ 为例,一起揭开这场视觉魔术的幕后秘密!从前端代码到流畅的用户界面,每一步都藏着超有意思的细节,也是我们打造高性能前端应用的核心基石~快跟上我,一起探索这场奇妙的页面诞生记吧!🚀
当我们打开 www.baidu.com/ 如何渲染出下面的界面
以 Chrome 浏览器为例

1、浏览器的多进程架构
首先浏览器会分配几个进程,页面的渲染是多个进程之间相互配合的结果


- 浏览器进程:主要负责界面显示、用户交互,同时提供存储等功能
- 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,包含排版引擎 Blink、V8 JS 引擎,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程
- 网络进程:主要负责页面的网络资源加载
- GPU 进程:只有一个 GPU 进程,为所有浏览器 Tab 标签页服务。它负责与 GPU 硬件直接通信,执行页面光栅化和合成操作
- Spare Renderer(Chrome 备用渲染进程): 是一个在后台提前准备好的、空闲的网页渲染进程,目的是提升用户体验和浏览速度而采用的一种优化策略
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离
页面的渲染需要多进程之间相互搭配,重点在于浏览器进程、渲染进程、网络进程、GPU进程之间的配合
2、HTTP 请求过程
浏览器中的 HTTP 请求从发起到结束一共经历了如下八个阶段:
-
构建请求:
GET https://www.baidu.com/ HTTP2 -
查找缓存:当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载

markdown
* 缓存方式:强制缓存/协商缓存 200 304 Cache-Control/Last-Modified/ETag
* 缓存分类:Memory Cache/Disk Cache/Service Worker/Push Cache(HTTP 2)
- 准备 IP 和端口:请求 DNS 系统返回域名对应的 IP、端口
www.baidu.com:170.114.52.116:443

-
等待 TCP 队列:同一个域名同时最多只能建立 6 个 TCP(http1.1) 连接,其他请求进入排队等待
-
建立 TCP 连接:三次握手,客户端和服务器总共要发送三个数据包以确认连接的建立
-
发起 HTTP 请求:HTTP 请求行、请求头、请求体
-
服务器处理请求/服务器返回请求:返回响应行、响应头、响应体
-
断开连接:执行"四次挥手"来保证双方都能断开连接
3、从输入 URL 到页面展示
整个过程需要各个进程之间的配合,如下图所示

-
用户输入:浏览器进程判断输入的关键字是搜索内容,还是请求的 URL,整合成完整的 URL
-
URL 请求过程:浏览器进程会通过 IPC 把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会发起请求,执行步骤二。这里补充几个重要的知识点
-
响应状态码:301/302 200 304 400 500...
-
响应类型(Content-Type):服务器返回的响应体数据是什么类
bash1. text/html:服务器返回的数据是 HTML 格式,触发渲染流程 2. application/octet-stream:数据是字节流类型的,触发下载流程 3. application/json:JSON 字段,前后端常见类型 4. text/css|text/javascript |application/javascript:CSS 文件、JS 文件 -
-
准备渲染进程:网络进程将响应信息传递给浏览器进程,如果是 text/html,则渲染进程准备接收数据
-
提交文档:文档可以理解为响应体即返回的 HTML 内容
- 所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程
- 渲染进程接收到"提交文档"的消息后,会和网络进程建立传输数据的"管道"
-
渲染阶段:当渲染进程接收到网络进程的响应数据时,便开始页面解析和子资源加载了,边加载边解析
接下来就进入到另外一个部分,渲染进程将网络进程接收到的 HTML + CSS + JS 生成页面,如下图所示

4、页面渲染流程 - 渲染流水线
当网络进程和渲染进程建立管道链接时,渲染进程便可以接收 HTML CSS JS 等数据,接下来渲染进程开始渲染工作。但渲染机制过于复杂,在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段显示成页面,这样的处理流程叫做渲染流水线

按照渲染的时间顺序,流水线可分为如下几个子阶段:
构建 DOM 树、样式计算、布局阶段、分层、绘制、栅格化和合成
4.1 构建 DOM 树
浏览器无法直接理解和使用服务器返回的 HTML,所以需要将 HTML 转换为浏览器能够理解的结构------DOM 树, HTML 经过 HTML 解析器解析,生成树状结构的 DOM

4.2 样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成
- 把 CSS 转换为浏览器能够理解的结构:styleSheets

- 转换样式表中的属性值,使其标准化

- 计算出 DOM 树中每个节点的具体样式:继承规则和层叠规则

4.3 布局阶段
现在我们有 DOM 树和 DOM 树中元素的样式,接下来需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局
创建布局树:只包含可见元素的的布局树
- 渲染进程遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
- 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容、display:none
布局计算:计算布局树的几何位置

到这里我们完成了渲染流程的前三个阶段:DOM 生成、样式计算和布局,像下图一样

4.4 分层
我们看到的界面都是多个图层相互叠加的结果,类似于 PS 的图层一样
position:fixed、z-indexing 做 z 轴排序都会有新的图层
渲染引擎需要为节点生成专用的图层,并生成一棵对应的图层树(LayerTree),具有几个特点
- 如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
- 拥有层叠上下文属性的元素会被提升为单独的一层

4.5 图层绘制
渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示

- 绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等
- 而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制
- 所以在图层绘制阶段,产生的内容就是这些待绘制列表
4.5 栅格化(复杂)
新的名称:合成线程
合成线程 是渲染进程中独立于主线程的一个关键工作线程,它的主要目标是高效地将页面的不同部分合成最终的图像,并发送给GPU进行绘制,从而确保滚动、动画等操作极其流畅
第 1 步:当图层的绘制列表准备好之后,主线程会把图层绘制列表提交给合成线程

两个概念
- 视口:通常页面很长,用户能看到的部分叫做视口
- 图块:合成线程会将图层划分为图块,这些图块的大小通常是 256 * 256 px 或者 512 * 512 px
第 2 步:合成线程会为每个图块生成对应的光栅化任务
- 这个任务包含了"图块的位置、图块所属图层的那部分绘制记录" 的信息
- 由渲染进程的栅格化线程执行,优先处理视口附近的图块

第 3 步:提交任务到 GPU 进程
- 栅格化线程将这些光栅化任务以及相关的绘制记录(作为数据源)放入一个队列中
- 然后,它通过 IPC 将任务队列提交给 GPU 进程。进行 GPU加速

第 4 步:GPU 进程与 GPU 硬件执行光栅化
- GPU 进程接收任务:GPU 进程收到来自渲染进程(合成线程)的任务队列。
- 进行光栅化操作
什么是光栅化?
简单说,就是将矢量图形(如绘制记录中的矩形、路径、文字命令)转换为屏幕上的像素(位图)的过程。GPU 进程会驱动 GPU 硬件来执行实际的光栅化工作。它利用 GPU 上强大的、高度并行化的光栅化器来高效完成此任务
- 结果存储:光栅化后的每个图块存储在 GPU 的显存中
4.6 合成和显示
第 1 步:生成绘制(DrawQuad)指令
- 当所有(或优先的)图块都光栅化完成后,合成线程会收集每个图块在 GPU 显存中的位置信息(ID)
第 2 步:和浏览器进程通信
- 浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的合成图像
- 然后浏览器进程和 GPU 进程通信,发送合成帧
第 3 步:GPU 进程驱动合成
- GPU 进程接收到合成帧后,会向 GPU 发送最终的绘制命令
- GPU 根据合成帧的指令,将各个图块纹理(就像一张张图片)绘制到屏幕缓冲区的正确位置上
以上这个过程就是合成 ,它通常通过非常高效的硬件加速操作(如纹理映射)来完成
显示:最终,屏幕的显示控制器从帧缓冲区读取数据,并将像素点亮,用户就看到了最终的页面

结合上图,一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式
- 创建布局树,并计算元素的布局信息
- 对布局树进行分层,并生成分层树
- 为每个图层生成绘制列表,并将其提交到合成线程
- 合成线程将图层分成图块,并在光栅化线程池中配合 GPU 将图块转换成位图
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
5、拓展内容
5.1 页面的重新渲染(重排、重绘)
来介绍以下前端常说的渲染流水线中两个概念,重排、重绘
1、重排:更新元素的几何位置
例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的

常见的引起重排的方式
-
页面首次渲染
-
浏览器窗口大小发生变化
-
元素的内容发生变化
-
元素的尺寸或者位置发生变化
-
元素的字体大小发生变化
-
添加或者删除可见的DOM元素
2、重绘:更新元素的绘制属性
比如通过 JavaScript 更改某些元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些

常见的引起重绘的方式
- 改变颜色 (
color,background-color) - 改变可见性 (
visibility) - 改变边框样式(如
border-style,但不改变border-width) - 文本阴影 (
text-shadow)
5.2 性能优化
分为两个阶段:加载阶段和交互阶段、关闭阶段
加载阶段

降低关键资源个数
降低关键资源大小:压缩代码、去除代码注释等
降低关键资源需要多少个 RTT:使用 CDN、缓存
交互阶段
