blog
blog copied to clipboard
从浏览器关键渲染路径聊起
(一)浏览器关键渲染路径-CRP
关键渲染路径是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。优化关键渲染路径可提高渲染性能。
我们这里只尽量精简描述这个渲染过程,更具体的可以去看MDN-关键渲染路径。

在浏览器接收html时(浏览器收到数据的第一块时就可以开始解析收到的信息):
-
首先解析 HTML 并构建 DOM 树;
- 遇到非阻塞资源(如图片)会请求这些资源并且继续解析。
- 遇到阻塞资源如
- 需要重点提CSS资源:CSS不会阻塞 HTML 的解析,但会阻塞 JavaScript(CSS后面的JavaScript需要等CSS下载完成),因为 JavaScript 经常用于查询元素的 CSS 属性。
- JavaScript后面的CSS不会阻塞本JavaScript执行,并且本JavaScript查询的CSS属性也是不包含后面CSS内容的。
- 预加载扫描器帮助提前下载资源。
-
Style(也叫构建Render树)
- 渲染树包括了内容和样式:DOM 和 CSSOM 树结合为渲染树(或者叫样式树)。为了构造渲染树,浏览器检查每个节点,从 DOM 树的根节点开始,并且决定哪些 CSS 规则被添加。
-
Layout(布局)
- Layout 是确定渲染树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。
- Reflow 回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。
- 第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。
-
Paint
- 最后一步是将像素绘制在屏幕上。
- 还有一步叫 Compositing,即不同层的合成,但讨论CRP时,把它作为Paint的一部分即可。讨论Reflow/Repaint时可以单独拆出讨论。
CRP 流程整体比较简单直接,我们讨论其中比较有意思的几个细节:
1. script 的 defer VS async
具有 async 属性的脚本在完成下载后和 load 事件之前第一时间执行。
- 不能保证多个async脚本的执行顺序(谁先加载完谁先执行);
- 如果脚本下载完,解析器还在工作,意味着会中断DOM构建。

具有 defer 属性的脚本在 HTML 解析完全完成之后执行,但在 DOMContentLoaded 事件之前执行。
- 可以保证顺序;
- 不会阻塞解析器。

如果在加载过程中更早地运行脚本很重要,请使用 async。
2. 怎么优化CRP?
-
优化网络和资源本身(不局限于CRP):
- 利用CDN,利用HTTP2等,优化网络速度;
dns-prefetch,preconnect等优化dns、tcp建立速度;- 资源压缩,
tree-shaking等清除无用代码等等,减小资源体积; - http缓存。
-
通过异步、延迟加载或者消除非关键资源来减少关键资源的请求数量,如
defer/async/preload/prefetch。- 用
defer/async来消除js对DOM构建的阻塞; - 通过代码分割,异步加载非首屏部分等等。
- 用
-
通过区分关键资源的优先级来优化被加载关键资源的顺序,来缩短关键路径长度。
preload/prefetch等优化资源加载顺序;- 资源放在合适位置:关键资源如CSS等放在
<head>;js放在<body>底部。
3. Reflow/Repaint 以及相关的优化
任何样式更新、DOM增删等等,造成布局(Layout)需要重新计算就称为回流(Reflow)。而不影响布局的节点样式/几何属性变更就是重绘(Repaint)。
回流严重影响性能,需要优化:
- 将频繁重绘或者回流的节点独立为新图层:
will-change/translate3d/translatez等等; - 将频繁重绘或者回流的节点布局改为
position:absolute/fixed; - visibility 替换
display: none,transform替代top等; - 避免频繁操作DOM/更新样式,尽量批量读取/操作DOM样式。
(二)浏览器进程/线程角度看渲染流程以及 event loop
现代浏览器一般是多进程架构,以Chrome为例,一般会有以下进程:
- 一个 Browser 进程(可以兼作 GPU 进程或者 Renderer 进程,即没有独立的 GPU 或者 Renderer 进程);
- 一个 GPU 进程;
- 多个 Renderer 进程,每个 Renderer 进程对应一个页面。
Renderer 进程:
- Renderer 线程:运行Blink,称之为内核主线程,负责JS的解析执行,HTML/CSS解析,DOM操作,排版,图层树的构建和更新等任务;
- Compositor 线程:运行Layer Compositor;
- 其它线程,包括 worker 线程等等。
从架构上就可以看出,当 Renderer 线程被 JS 阻塞(忙于执行JS)时,卡顿掉帧是必然的。为了达到60FPS,必须限制JS运行时间、减少复杂的样式更新。
一桢内浏览器会做什么?

上面的图其实也某种程度就是浏览器的 event loop 流程。
下面是几点需要注意的细节:
- JS 和 UI更新在同一个线程,所以JS长时间运行会阻塞UI;
- 当页面invisible时,帧数会主动降低(可能1000毫米以上调用一次timer);
- 善用 requestAnimationFrame。