导航流程
浏览器从输入URL到页面展示间到底发生了什么?这是一个非常经典的前端面试题目了,这个题目非常考验前端功底,整个流程涉及到网络请求和渲染流程两大块内容,那我们先回顾下导航流程吧。
1、浏览器进程构建完整的URL
- 浏览器进程会检查输入的URL,组装协议,构建完整的URL
- 浏览器进程通过进程间通信(IPC)把URL请求发送给网络进程
2、网络进程发起URL请求
- 查找本地缓存是否有效,如果有效,则使用本地缓存;如果无效,则进入网络请求流程
- 网络请求的第一步是DNS解析,获取请求域名的IP地址
- 和服务器建立TCP连接,并构建请求信息
- 服务器接收请求后,会构建响应信息
- 浏览器接收到响应后,网络进程会解析响应信息,若状态码是301/302,则会重定向到新地址,重新发起新的URL请求
- 浏览器根据响应数据类型(Content-Type)进行处理
3、浏览器进程向渲染进程提交文档
- 浏览器进程接收到响应后,会开始准备渲染进程,同一站点(同根域名、同协议)会复用一个渲染进程
- 渲染进程准备完毕后,浏览器进程会和渲染进程建立通信,传输文档
4、渲染进程开始解析页面和加载子资源,完成页面的渲染
这个过程就是渲染流程了,下面我们具体地了解下这个流程。
渲染流程
1、构建DOM树
为什么要构建DOM树?因为浏览器无法识别HTML,所以要把HTML解析成浏览器能识别的数据结构——DOM树。下图所示是一个DOM树。

2、构建CSSOM树
同样地,浏览器也无法识别CSS,所以浏览器会先将CSS解析成浏览器能识别的数据结构——styleSheets。
接着,浏览器会将样式的属性值转化成标准值。因为标准值才容易被渲染引擎理解和使用。

最后,计算DOM树中每个节点的样式,生成最终的CSSOM树。这个过程涉及到CSS的继承规则和层叠规则。

3、构建布局树
现在,我们拥有了DOM和CSSOM这两个独立的对象,浏览器将这两个对象模型结合,构建布局树。
首先,浏览器会遍历所有可见的元素(像head
这类不可见的标签或者dispaly
设置为none
的元素等会被排除在布局树外),接着,找到节点所适配的样式并应用。

此时浏览器还不知道每个节点的位置信息,所以浏览器会遍历布局树,计算每个节点的位置信息,这就是计算布局。
4、分层和绘制
经过计算布局之后,并不是立马进行绘制,而是会为有3D或透视变换、z轴排序等复杂效果的节点创建图层,并生成图层树,这样做的目的是方便地实现复杂效果。浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
那什么情况下会创建图层呢?
- 有层叠上下文(明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜)
- 需要剪裁(overflow: hidden;溢出部分被剪裁)
有了图层树之后,接下来就是绘制图层了。
浏览器会把一个图层拆分为一个个小的绘制指令,然后将指令按照顺序排成一个列表。绘制过程和使用canvas进行绘制图类似。
接着,浏览器会将图层划分为图块,这么做的目的是因为视口显示的内容有限,如果直接将整个结构进行绘制开销比较大,所以浏览器会优先将视口内的图块转为位图,这个过程叫栅格化。
最后,将位图合成,浏览器开始显示。
小结
从输入URL到页面呈现的整体流程:
- 浏览器进程构建完整URL,并通过进程间通信将URL提交给网络进程
- 网络进程:
- 检查缓存
- DNS解析
- 建立TCP连接(三次握手)
- 发送请求数据
- 接收响应数据,并根据响应数据类型进行解析
- 网络进程将处理好的数据提交给浏览器进程,浏览器进程准备好渲染进程
- 渲染进程:
- 将HTML解析为DOM树
- 将CSS解析为CSSOM树
- 将DOM树和CSSOM树构建成布局树
- 进行分层和绘制
如下图所示,HTML的渲染过程如下:

渲染流程的特点
回流和重绘
这两个是渲染过程中比较重要的概念了,了解其概念并进行合理应用,可以提升性能。
回流
当元素的几何属性(尺寸)、隐藏属性等改变而触发重新布局的渲染,这个过程就是回流。回流需要更新完整的渲染流程(布局-分层-绘制-图块-栅格化-合成-显示),所以开销较大,需要尽量避免。
触发回流的属性
- 盒子模型相关属性(width、padding、margin、display、border等)
- 定位属性和浮动(position、top、float等)
- 文字结构(text-align、font、white-space、overflow等)
重绘
当元素的外观、风格等属性发生改变但不会影响布局的渲染,这个过程就是重绘。重绘省去了布局和分层阶段(绘制-图块-栅格化-合成-显示),所以性能比回流要好。回流必将引起重绘,重绘不一定会触发回流。
触发重绘的属性
color、border-style、background、outline、box-shadow、visibility、text-decoration
避免重绘和回流
频繁触发重绘和回流,会导致UI频繁渲染,最终导致性能变差。所以要尽量避免重绘和回流:
- 避免使用触发重绘和回流的CSS属性
- 将频繁重绘回流的元素创建为一个独立图层
技巧
- 使用transform实现效果:可以避开回流和重绘,直接进入合成阶段(图块-栅格化-合成-显示)
- 用opacity替代visibility:visibility会触发重绘
- 使用class替代DOM频繁操作样式
- DOM离线后修改,如果有频繁修改,可以先把DOM隐藏,修改完成后再显示
- 不要在循环中读取DOM的属性值:offsetHeight会使回流缓冲失效
- 尽量不要使用table布局,小改动会造成整个table重新布局
- 动画的速度:200~500ms最佳
- 对动画新建图层
- 启用GPU硬件加速:启用translate3D
HTML解析的特点
顺序执行、并发加载
- 顺序执行:HTML的词法分析是从上到下,顺序执行
- 并发加载:当 HTML 解析器被脚本阻塞时,解析器虽然会停止构建DOM,但仍会识别该脚本后面的资源,并进行预加载。
- 并发上限:浏览器对同域名的并发数是有限制的(HTTP/2则没有这个限制)
阻塞
css阻塞
- css在head中阻塞页面的渲染:避免页面闪动
- css会阻塞js的执行:CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。
- css不阻塞外部脚本的加载
默认情况下,CSS会阻塞渲染,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。不过,使用媒体查询可以让CSS资源不在首次加载中阻塞渲染。
js阻塞
- 直接引入的js会阻塞页面的渲染:当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行
- js不阻塞资源的加载
- js顺序执行,会阻塞后续js的执行
- js可以查询和修改 DOM 与 CSS
改变js的阻塞
defer
和async
属性可以改变js的阻塞情形,不过这两个只对src方式引入的script有效,对于inline-script无效。
defer
表示延迟执行,浏览器会异步地加载该脚本并且不会影响到后续DOM的渲染,该脚本将在文档完成解析后,DOMContentLoaded
事件触发前执行。对动态嵌入的脚本使用 async=false
来达到类似的效果。
async
表示异步执行,浏览器会异步地加载脚本并在允许的情况下执行。与 defer 的区别在于,无论是 HTML 解析阶段还是DOMContentLoaded
触发之后,如果脚本加载完成,就会开始执行。需要注意的是,这种方式加载的 JavaScript 依然会阻塞load
事件。
DOMContentLoaded
MDN的解释:当初始的 HTML 文档被完全加载和解析完成之后,
DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。
load
MDN的解释:load 应该仅用于检测一个完全加载的页面
当一个资源及其依赖资源已完成加载时,将触发load事件。