渲染优化之浏览器关键渲染路径

一、关键渲染路径

关键渲染路径:JavaScript -> Style -> Layout -> Paint -> Composite

第一步:浏览器构建对象模型

在这一步中,浏览器会解析 JavaScript 和 CSS,构建 DOM 对象和 CSSOM 对象。

  1. 构建DOM对象
    HTML -> DOM

  2. 构建CSSOM对象
    CSS -> CSSOM

第二步:浏览器构建渲染树

DOM + CSSOM = Render tree

通过合并 DOM 、CSSOM 对象,构建渲染树,从而得知网页上所需要的节点

第三步:布局(Layout)

基于渲染树,将页面上每个节点的精确位置和大小计算出来,得到每个节点的“盒模型”。

第四步:绘制(Paint)

绘制就是像素化每个节点的过程。

第五步:复合(Composite)

浏览器其实是分层显示的,就像 Photoshop 中的图层一样,最后一步就是将
各层叠在一起。

复合在浏览器中是由专门的复合线程(Compositer thread)来做的,它将页面拆分成图层进行绘制再进行复合。

小技巧:可以通过 Chrome Dev Tools 中的 Performance -> frame -> layers 来查看图层

二、浏览器每帧的生命周期

browser_frame
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。 1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms。
Life of a frame

那么浏览器每一帧都需要完成哪些工作?

  1. 处理用户的交互(输入了一些事件)
  2. 解析执行 JS
  3. 帧开始。窗口尺寸变更,滚动,动画等等
  4. 执行 requestAnimationFrame 和 IntersectionObserver 的回调函数
  5. 布局。重新计算样式,更新布局,执行 ResizeObserver 的回调
  6. 绘制。

requestIdleCallBack

上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。
Idle_callback

从上图也可看出,和 requestAnimationFrame 每一帧必定会执行不同,requestIdleCallback 是捡浏览器空闲来执行任务。 如此一来,假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下面 API 介绍)来保证执行。

API
var handle = window.requestIdleCallback(callback[, options])

  1. callback:回调,即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
    • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
    • timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
  2. options:目前 options 只有一个参数 timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。

参考:requestIdleCallback

通过浏览器每帧生命周期机制,我们可以将一些高频率的事件进行防抖,将其放入 requestAnimationFrame 的回调中执行(例如:鼠标滑动动画)。另外,一些低优先级的任务可以使用 requestIdleCallback 来执行(例如:日志上报)。

扩展阅读:React 时间调度

三、渲染优化

渲染优化的重点就是回流和重绘。

回流(reflow)

需要重新布局的就叫回流。

回流带来的影响很大,因为它要重新计算所有元素的尺寸和位置,这将导致重新渲染部分或全部文档。更改一个元素可以影响所有子元素、父元素和兄弟元素。

产生回流的操作有哪些呢?

  1. 添加/删除元素
  2. display: none
  3. offsetLeft, scrollTop, clientWidth
  4. 移动元素位置
  5. 修改浏览器大小、字体大小

重绘(repaint)

改变元素可见性但不影响布局的就会产生重绘。例如:background-color、visibility 以及 outline。

重绘非常昂贵,因为浏览器必须检查 DOM 中所有其他节点的可见性——因为被改变可见性的元素之下的元素的可见性有可能会改变(浏览器是按层渲染的)。

小技巧:Mac 下可通过 Shift + Command + P 调出功能检索框,输入“Show Rendering”并回车,找到“Paint flashing”并勾选,从而可以看到重绘区域被高亮。

如何优化?

(一)通过浏览器分层来避免重绘和回流

由于浏览器是分层渲染的,所以通过将元素置入新的分层,即不会影响到主文档层中的元素的布局和绘制,也就不会产生重绘和回流。也就是说,仅仅会影响到复合。

哪些样式仅影响复合呢?(不会产生重绘和回流)

  1. 位置变化
    使用 transform: translate(x, y)
  2. 大小变化
    使用transform: scale(n)
  3. 旋转
    使用transform: rotate(n deg)
  4. 透明度
    使用opacity:0...1

通过 CSS willChange 属性也可以将元素放到新的图层,从而避免主图层的重绘和回流。

(二)采用虚拟 DOM 将视图变化合并,从而减少重绘和回流的次数
(三)避免 layout thrashing(布局抖动)

所谓布局抖动是JavaScript多次强制读写DOM,导致回流,然后浏览器必须重绘页面,这会造成加载页面时产生很大的延迟,视觉上表现为布局抖动。

避免布局抖动的方法

  1. 避免回流的操作
  2. 读写分离

也可以使用开源方案 fastdom 来解决布局抖动的问题。

(四)JavaScript 动画采用 requestAnimationFrame

由于 requestAnimationFrame 会在每一帧中执行,所以它能够尽量保证动画的流畅度;同时,它会在每一帧的 Layout 和 Paint 之前执行,所以它也比较适合操作 DOM。

用户头像
登录后发表评论