一、关键渲染路径
关键渲染路径:JavaScript -> Style -> Layout -> Paint -> Composite
第一步:浏览器构建对象模型
在这一步中,浏览器会解析 JavaScript 和 CSS,构建 DOM 对象和 CSSOM 对象。
-
构建DOM对象
HTML -> DOM -
构建CSSOM对象
CSS -> CSSOM
第二步:浏览器构建渲染树
DOM + CSSOM = Render tree
通过合并 DOM 、CSSOM 对象,构建渲染树,从而得知网页上所需要的节点。
第三步:布局(Layout)
基于渲染树,将页面上每个节点的精确位置和大小计算出来,得到每个节点的“盒模型”。
第四步:绘制(Paint)
绘制就是像素化每个节点的过程。
第五步:复合(Composite)
浏览器其实是分层显示的,就像 Photoshop 中的图层一样,最后一步就是将
各层叠在一起。
复合在浏览器中是由专门的复合线程(Compositer thread)来做的,它将页面拆分成图层进行绘制再进行复合。
小技巧:可以通过 Chrome Dev Tools 中的 Performance -> frame -> layers 来查看图层
二、浏览器每帧的生命周期
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。 1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms。
那么浏览器每一帧都需要完成哪些工作?
- 处理用户的交互(输入了一些事件)
- 解析执行 JS
- 帧开始。窗口尺寸变更,滚动,动画等等
- 执行 requestAnimationFrame 和 IntersectionObserver 的回调函数
- 布局。重新计算样式,更新布局,执行 ResizeObserver 的回调
- 绘制。
requestIdleCallBack
上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。
从上图也可看出,和 requestAnimationFrame 每一帧必定会执行不同,requestIdleCallback 是捡浏览器空闲来执行任务。 如此一来,假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下面 API 介绍)来保证执行。
API
var handle = window.requestIdleCallback(callback[, options])
- callback:回调,即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
- didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
- timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
- options:目前 options 只有一个参数 timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。
通过浏览器每帧生命周期机制,我们可以将一些高频率的事件进行防抖,将其放入 requestAnimationFrame 的回调中执行(例如:鼠标滑动动画)。另外,一些低优先级的任务可以使用 requestIdleCallback 来执行(例如:日志上报)。
扩展阅读:React 时间调度
三、渲染优化
渲染优化的重点就是回流和重绘。
回流(reflow)
需要重新布局的就叫回流。
回流带来的影响很大,因为它要重新计算所有元素的尺寸和位置,这将导致重新渲染部分或全部文档。更改一个元素可以影响所有子元素、父元素和兄弟元素。
产生回流的操作有哪些呢?
- 添加/删除元素
- display: none
- offsetLeft, scrollTop, clientWidth
- 移动元素位置
- 修改浏览器大小、字体大小
重绘(repaint)
改变元素可见性但不影响布局的就会产生重绘。例如:background-color、visibility 以及 outline。
重绘非常昂贵,因为浏览器必须检查 DOM 中所有其他节点的可见性——因为被改变可见性的元素之下的元素的可见性有可能会改变(浏览器是按层渲染的)。
小技巧:Mac 下可通过 Shift + Command + P 调出功能检索框,输入“Show Rendering”并回车,找到“Paint flashing”并勾选,从而可以看到重绘区域被高亮。
如何优化?
(一)通过浏览器分层来避免重绘和回流
由于浏览器是分层渲染的,所以通过将元素置入新的分层,即不会影响到主文档层中的元素的布局和绘制,也就不会产生重绘和回流。也就是说,仅仅会影响到复合。
哪些样式仅影响复合呢?(不会产生重绘和回流)
- 位置变化
使用transform: translate(x, y)
- 大小变化
使用transform: scale(n)
- 旋转
使用transform: rotate(n deg)
- 透明度
使用opacity:0...1
通过 CSS willChange
属性也可以将元素放到新的图层,从而避免主图层的重绘和回流。
(二)采用虚拟 DOM 将视图变化合并,从而减少重绘和回流的次数
(三)避免 layout thrashing(布局抖动)
所谓布局抖动是JavaScript多次强制读写DOM,导致回流,然后浏览器必须重绘页面,这会造成加载页面时产生很大的延迟,视觉上表现为布局抖动。
避免布局抖动的方法
- 避免回流的操作
- 读写分离
也可以使用开源方案 fastdom 来解决布局抖动的问题。
(四)JavaScript 动画采用 requestAnimationFrame
由于 requestAnimationFrame 会在每一帧中执行,所以它能够尽量保证动画的流畅度;同时,它会在每一帧的 Layout 和 Paint 之前执行,所以它也比较适合操作 DOM。