Roger Leung‘s Epcot

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021
Roger Leung‘s Epcot

Choose mode

  • dark
  • auto
  • light
Blog
Note
Github (opens new window)
author-avatar

Roger Leung ( z3rog )

18

Article

20

Tag

Blog
Note
Github (opens new window)
  • 首页
  • 框架与工具链

    • Vue 3
    • Vue 2
    • Webpack 4
  • 前端性能优化

    • 性能优化的必要性
    • 性能指标
    • 基本手段
    • 离线缓存
  • 浏览器机制

    • 架构
    • 导航
    • 渲染机制
    • 缓存机制
  • 网络协议

    • TCP 协议
    • HTTP 协议
    • HTTPS 协议
    • HTTP 2 协议
    • HTTP 3 协议
  • 其他

    • V8 中的快慢属性与快慢数组
    • V8 解析执行 JavaScript 流程简述
    • V8 的垃圾回收机制
    • 100 行代码写一个破产版 Vite
    • 浅谈微前端

渲染机制

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021

渲染机制

Roger Leung ( z3rog ) 2019-09-08 Chrome

浏览器渲染机制是一个前端的必备知识。

browser-structure

总体来说,现代浏览器可以分为 7 个抽象部分

  • User Interface:用户界面
  • Browser Engine: 浏览器引擎
  • Rendering Engine: 渲染引擎
  • Networking Component: 网络组件
  • JavaScript Engine: JS 引擎
  • UI Backend:显示后端
  • Data Storage:数据存储层(或称 Data Persistence,数据持久层)

顾名思义,渲染引擎用于处理浏览器页面的渲染。下面是一张浏览器渲染引擎处理流程图。

# 渲染流程

renderer-process

黑盒中的四个步骤对应的是 流程图 中 Processing 的部分。当网络进程处理好数据后,通知浏览器线程,浏览器线程调度渲染进程执行渲染。

大体流程分别为:

  • HTML Parser 将 HTML 对象化,解析为 DOM 树,将 CSS 解析为 CSSOM Tree
  • 建造 Render Tree,将 DOM 树中的各个节点附着上 CSSOM 树中对应节点的 style 信息(DOM Tree + CSSOM Tree = Render Tree)
  • 根据 Render Tree 生成显示器中的具体布局信息描述(Layout Tree),比如确切的位置坐标
  • UI Backend(显示后端)将信息传递至 GPU,后续 GPU 参与图像绘制,显示到屏幕中

# HTML 解析

html-parsing

解析 DOM 的过程中,预加载扫描器会同时运行,当发现有诸如图片、CSS、JS 等子资源时,通知网络线程进行加载。当 HTML Parser 找到 <script> 标签后,若标签没有 defer 或 async 属性(即非异步脚本)会暂停执行 HTML 的解析,且必须加载、解析并执行 JS 代码后,再继续解析后续标签,以防止有代码直接改变整个 DOM 结构(如 document.write())而费时做不必要的解析。

总结

  1. CSS 的加载不会阻塞 DOM 树的解析(DOM 树不依赖 CSS),但非异步 JS 文件的加载会阻塞 HTML 的解析(JS 可能改变 DOM 结构),所以一般 JS 文件放在 body 最下方。

  2. defer / async

    • <script defer> 当 DOM 树被解析完成后,脚本再执行,且执行时机一定在 DOMContentLoaded 之前完成
    • <script async> 与 defer 一样,脚本不会阻塞 DOM 树的解析,但当脚本内容被加载完成后会立即执行

# 样式计算

style-calculation 渲染主进程解析 CSS 生成 CSSOM Tree,附着于 DOM 树中,形成 Render Tree,但此时只有各个元素的样式信息,并未精确到布局(位置),这时候需要继续计算。

# 生成布局树

layout-tree

渲染主进程遍历 Render Tree,根据 Render Tree 生成布局树 Layout Tree,Layout Tree 中包含诸如 x y 坐标元素边框大小之类的信息,结构与 DOM 树类似,但仅包含页面上可见内容有关的信息。

注意

具有 display: none 属性的元素不属于布局树,但是具有 visibility: hidden 属性或类似内容伪类(::before { content: 'Hi!' })的元素都包含在布局树中。

# 绘制

paint-order

生成布局树后仍然不足以渲染一个页面。 比如绘制一幅画的时候,绘制的顺序决定了成画的上下层级,先绘制层级高的,后面会被覆盖。

DOM 也是这样的。

所以,绘制不能以 DOM 顺序来执行。此时需要遍历 Layout Tree 生成一系列的 Paint Records(绘制记录),你可以认为 Paint Records 是一个单向链表,遍历链表可获得正确的绘制顺序。

paint-records

当布局树发生某些变更,Paint Records 需要同步响应,生成新的绘制顺序以保证绘制过程不会出错。

# 分层

renderer-layer-tree

浏览器渲染主进程需要遍历 Layout Tree 生成 Layer Tree。一般情况下,Layer 中的元素相对独立,那么更新该 Layer 的速度会更快,你可以通过增加一个 will-change 的 CSS 属性来告诉浏览器这个元素需要独立地分层,但相应地,Layer 数量增多可能会降低合成的性能,所以页面 Layer 的数量需要做好管理,才能让页面的渲染性能有提升。

renderer-chrome-layers 我们可以打开 Chrome 的 Layers 看到页面中的所有 Layer, Details 中包含当前 Layer 的 Size 及 Compositing Reasons。Compositing Reasons 会列举产生 Layer 的原因,包括但不限于:

  • will-change CSS 属性
  • 3D 变换(translateZ、translate3d) 属性
  • 使用了硬件加速的 <video> <canvas> 标签
  • fixed 或 sticky 定位
  • 有可滚动的内容
  • 需要渲染在一个 Composited Layer 之上的元素

点击 Paint Profiler 查看前文提到的 Paint Records。

# 合成

当走到合成这一步时,浏览器已经知道了文档的结构、各个元素的样式、页面的坐标 及 绘制顺序 了,接下来需要将以上所有信息转换为屏幕上能够显示的像素点信息,称为 Rasterizing (光栅化)。

旧式浏览器常用的手段是:在可视区域内将元素光栅化,随着用户滚动页面,不断调整光栅化的区域,继续光栅化并讲内容填充到缺失的部分。见下:

现代浏览器使用更高效的手段,这种手段称为 Compositing (合成)。见下:

合成时,浏览器将页面的各个部分分成若干个 Layer (图层),对各个 Layer 单独进行光栅化,随后在 Compositor( 合成器) 线程中对各个 Layer 进行合成,可能由多个 Render Layer 合成为 1 个 Composited Layer,也可能一个 Render Layer 独立成为 Composited Layer 。因为已经提前对各个 Layer 进行了光栅化,如果页面发生了滚动,只需要再次对可视区域进行合成(每次合成的结果称为 Frame,即 帧 ),而无需像老式浏览器一样再次光栅化。


raster-tiles-to-gpu


上图展示的过程:合成器线程将图层划分块后发送给光栅化线程,光栅化线程对每个图块作光栅处理后,将图块信息存储到 GPU 的内存中,随后 GPU 进行图像的浮点数运。

合成器线程可以让光栅化线程优先处理视野区域(或附近)的图块,因为线程与线程之间是独立的,这样能保证视野区的图像最先被 GPU 处理完毕,不必等待所有图层,甚至当前图层的其余图块。


compositing-frame

光栅化后的各个小块称为 tile,合成器线程收集连续的一系列 tile 作为一个集合,并整合它们的信息,形成一个又一个的 Draw Quad (绘制四边形),绘制四边形包含图块在内存中的位置等等信息。页面所有 Draw Quad 的集合被称为 Compositor Frame (合成帧)。生成合成帧后渲染进程遍将帧信息提交到浏览器进程。

与此同时,其他渲染进程或者浏览器进程都有可能源源不断地生成合成帧,这些合成帧都会被发送到 GPU 中。

合成的时候不需要渲染(主)进程的参与,所以更推荐使用不会导致浏览器重绘或重排的动画,这样就不需要重新计算 Render Tree 和 Layout Tree,也就不需要渲染(主)进程的参与。

# 重排与重绘

前面已经讲过渲染的步骤,显然,绘制是在布局之后的。如果页面发生重排,必然发生重绘,但发生重绘不一定会重排。 当页面的尺寸导致页面布局、元素的几何位置发生变化,就会触发重排。

注意

读元素的属性值同样会触发重排。引擎需要放弃原本可能会做的优化,马上执行计算并返回准确结果,所以读属性会导致相对较高的渲染性能开销,一般连续读取属性的代码建议写在一起。

可以打开 codepen (opens new window) 这个例子,将代码 copy 到本地进行实验。

我们先将代码中的 @keyframe 中所有 CSS 还原为 top / left,注释掉 transform。

reflow-infinite

随后将 Paint flashing 和 Layout Shift Regions 打开,可以看到有两个色块跟随着小球一直移动。对这段动画进行录制,一段时间后暂停,打开 Event Log 可以看到浏览器一直不断地 Schedules Style Recalculation -> Recalculate Style -> Layout -> Update Layer Tree -> Paint -> Composite Layers,说明一直在触发重排与重绘。

如果使用 translate 代替 CSS 中的 top/ left,惊喜地发现,重排消失了,GPU 的负载也更低:

no-reflow

前面 分层 也提到过,使用 translate3d 对绘制会有性能的提升,此处同样适用。