渲染性能的优化,需要的是一条从底层原理出发的路径,在渲染性能优化方向上,面对纷繁复杂的问题时,有更加精准和明确的依据和更有价值的方案。
性能的本质可以说是在体验、处理能力和功耗三个方向上找到平衡点。首先,前端包含了渲染和计算两个部分。渲染部分由 HTML 和 CSS 共同定义,交由浏览器进行渲染,浏览器确实屏蔽了大部分连接到底层能力的部分。但随着 WebGL、WebGPU 等全新 API 暴露,前端也能具有操作底层渲染的能力。计算部分由 JavaScript 定义,交由脚本引擎执行,因此,脚本引擎屏蔽了大部分连接到底层能力的部分,同时,脚本引擎基本都用虚拟机屏蔽底层指令集和硬件差异。但随着 Node.js 和 WASM 等技术让部分程序 Local 化本地执行,以及 V8 引擎用一些特殊策略让部分 JavaScript 被 Sparkplug 编译成 Local 代码执行,这种情况也会有所改变。
一个比较常用的性能优化策略:时间换空间 or 空间换时间。当空间压力大的时候(也就是存储压力大),可以用时间来换空间,典型的案例是:文件压缩。当时间压力比较大的时候(也就是计算压力大),可以用空间来换时间,典型案例是上文说到的:缓冲区(缓存),利用把长进程运算密集型任务分割成一系列可并发的子任务,并行计算后存储起来(缓冲区 / 缓存),再由 GPU 输出到 Display 上。
首先是要选择合适HTML 标记和 CSS 去构建要渲染的内容。针对渲染过程,常见的渲染问题有:卡顿、撕裂、掉帧……,卡顿、撕裂、掉帧通常都是渲染时间过长造成。渲染时间问题大多数情况下是耗费在 CPU 计算上,部分情况下是耗费在图形渲染上。
因此,从渲染过程来看,性能优化的本质在首先于降低 CPU、GPU 计算负载。其次,如果有条件通过不同的渲染内容构建方法去影响渲染过程的话,优先选择有 CPU、GPU 优化指令和专用电子电路加速的底层 API 来构建渲染内容。例如在 H.264 硬件加速普及的今天,是否应该用 X.265/H.265 就值得商榷。
在探讨渲染过程的时候,流畅性指标是首先需要关注的,根据 60Hz 刷新率下 16.6ms 的帧渲染速度,可以从时间角度定义 16.6msx2(双缓冲)、16.6msx3(三缓冲)的 CPU 和 GPU 处理时间,压缩渲染过程来保证流畅度。
OOPD(Out of Process Display Compositor,进程外 Display 合成器),它的主要目的是将 Display Compositor 从 Browser 进程迁移到 Viz 进程(也就是原来的 GPU 进程),Browser 则变成了 Viz 的一个 Client,Renderer 跟 Viz 建立链接(CompositorFrameSink)虽然还是要通过 Browser,但是建立链接后提交 CompositorFrame 就是直接提交给 Viz 了。Browser 同样也是提交 CompositorFrame 给 Viz,最后在 Viz 生成最终的 CompositorFrame,通过 Display 交由 Renderer 合成输出。
OOPR(Out of Process Rasterization,进程外光栅化)OOPR 跟目前的 GPU 光栅化机制的主要区别是:
-
在当前的 GPU 光栅化机制中,Worker 线程在执行光栅化任务时,会调用 Skia 将 2D 绘图指令转换成 GPU 指令,Skia 发出的 GPU 指令通过 CommandBuffer 传送到 Viz 进程的 GPU 线程中执行;
-
在 OOPR 中,Worker 线程在执行光栅化任务时,它直接将 2D 绘图指令(DisplayItemList)序列化到 Command Buffer 传送到 GPU 线程,运行在 GPU 线程的部分再调用 Skia 去生成对应的 GPU 指令,并交由 GPU 直接执行;
简而言之,就是将 Skia 光栅化的部分从 Renderer 进程转移到了 Viz 进程。当 OOPD,OOPR 和 SkiaRenderer 都开启后:
-
光栅化和合成都迁到了 Viz 进程;
-
光栅化和合成都使用 Skia 做 2D 绘制,实际上 Chromium 所有的 2D 绘制最终都交由 Skia 来做,由 Skia 生成对应的 GPU 指令;
-
光栅化和合成时,Skia 最终输出 GPU 指令都在 GPU 线程,并且使用同一个 Skia GrContext(Skia 内部的 GPU 绘图上下文);
这意味着,当 Skia 对 Vulkan,Metal,DX12 等其它 3DAPI 的支持完善后,Chromium 就可以根据不同的平台和设备,来决定 Skia 使用哪个 GPU API 来做光栅化和合成。而 Vulkan,Metal,DX12 这些更 Low Level 的 API 对比 GL API,可以带来更低的 CPU 开销和更好的性能。
纵观渲染过程,不同的 Low Level API 受到光栅化过程影响,光栅化过程受到合成器工作过程影响,合成器工作过程受到 Blink 对渲染内容处理的影响:
今天的程序更多的面对着复杂的、动态的场景,比如:数据动态加载和动态渲染、条件渲染、动效动画……等,因此,JavaScript 的干预也会导致渲染内容的变化,从而影响渲染的过程。
从原理上说 Blink 暴露 DOM 的 API 给 JavaScript 调用,如上图 createElement 一个 HTML 标记 append 到 document.body.firstChild 的 childNodes[1] 之上,也就是左图 P2,DOMTree 会发生变化导致整个渲染过程发生变化:
这也就是 Virtual-Tree 技术能够提高浏览器渲染性能的原理:合并 DOM Tree 的变更进行批量 bindings 从而降低重入渲染过程的几率和频次。
浏览器引擎良好的解耦了 V8 对 DOM 的干预,让这种干预局限在针对 HTML 标记本身上,但是,由于 JavaScript 的干预会导致 DOM 的变化,所以,同样会导致后续的渲染过程产生变化,因此,有时候合并 DOM Tree 的变更可能带来渲染结果的错误,在不了解渲染过程的前提下,使用 Virtual-Tree 出现一些渲染的问题可能更加难以定位和解决。
计算复杂度对渲染性能的影响
DOM、style、layout、comp.assign、paint(including prepaint)这个渲染过程中计算的部分的耗时会直接影响渲染性能。
关键渲染路径(Critical Rendering Path),简称:CRP
-
首先,一旦浏览器得到响应,它就开始解析它。当它遇到一个依赖关系时,它就会尝试下载它
-
如果它是一个样式文件(CSS 文件),浏览器就必须在渲染页面之前完全解析它(这就是为什么说 CSS 具有渲染阻碍性)
-
如果它是一个脚本文件(JavaScript 文件),浏览器必须:停止解析,下载脚本,并运行它。只有在这之后,它才能继续解析,因为 JavaScript 脚本可以改变页面内容(特别是 HTML)。(这就是为什么说 JavaScript 阻塞解析)
-
一旦所有的解析工作完成,浏览器就建立了 DOM 树和 CSSOM 树。将它们结合在一起就得到了渲染树。倒数第二步是将渲染树转换为布局。这个阶段也被称为重排
-
最后一步是绘制。它涉及到根据浏览器在前几个阶段计算出来的数据对像素进行字面上的着色
放到渲染引擎渲染页面的过程中,CRP 会经过下面几个过程:
-
处理 HTML 标记并构建 DOM 树
-
处理 CSS 标记并构建 CSSOM 树
-
将 DOM 树和 CSSOM 树合并大一个渲染树
-
根据渲染树来布局
-
将各个节点绘制到屏幕上
注意:当 DOM 或者 CSSOM 发生变化的时候(JavaScript 可以通过 DOMAPI 和 CSSOM API 对它们进行操作,从而改变页面视觉效果或内容)浏览器就需要再次执行上面的步骤。
优化页面的关键渲染路径(Critical Rendering Path)的方法:
-
减少关键资源请求数:减小使用阻塞的资源(CSS 和 JS),注意,并非所有资源是关键资源,尤其是 CSS 和 JS(比如使用媒体查询的 CSS,使用异步的 JS 就不关键了)。
-
减少关键资源大小:使用各种手段,比如减少、压缩和缓存关键资源,数据量越小,引擎计算复杂度越小。
-
缩短关键渲染路径长度。
在具体优化 CRP 时可以按下面的常规步骤进行:
-
对 CRP 进行分析和特性描述,记录关键资源数量、关键资源大小和 CRP 长度。
-
最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等。
-
优化关键资源字节数以缩短下载时间(往返次数),减少 CRP 长度。
-
优化其余关键资源的加载顺序,需要尽早下载所有关键资源,以缩短 CRP 长度。
优化HTML
编写有效的可读的 DOM:
-
用小写字母书写,每个标签都应该是小写的,所以请不要在 HTML 标签中使用任何大写字母
-
关闭自我封闭的标签
-
避免过渡使用注释(建议使用相应工具清除注释)
-
组织好 DOM,尽量只创建绝对必要的元素
减少 DOM 元素的数量(在 Blink 内核的 HTMLDocumentParser 处理中 Token 的个数和 DOM 元素数量息息相关,减少 Token 数量可以加快 DOM Tree 的构建,从而加快排版渲染首帧的速度),因为页面上有过多的 DOM 节点会减慢最初的页面加载时间、减慢渲染性能,也可能导致大量的内存使用。因此请监控页面上存在的 DOM 元素的数量,确保你的页面没有:
-
没有超过 1500 个 DOM 节点
-
DOM 节点的嵌套没有超过 32 级
-
父节点没有 60 个以上的子节点
优化CSS
-
CSS 类(class)的长度:类名的长度会对 HTML 和 CSS 文件产生轻微的影响(在某些场景下存有争议,详细参阅 CSS 选择器性能)
-
关键 CSS(Critical): 将页面 CSS 分为关键 CSS(Critical CSS)和非关键 CSS(No-Critical CSS),关键 CSS 通过<style>方式内联到页面中(尽可能压缩后引用),可以使用 critical 工具来完成(详细参阅关键 CSS)
-
使用媒体查询:符合媒体查询的样式才会阻塞页面的渲染,当然所有样式的下载不会被阻塞,只是优先级会调低。
-
避免使用 @import 引入 CSS:被 @import 引入的 CSS 需要依赖包含的 CSS 被下载与解析完毕才能被发现,增加了关键路径中的往返次数,意味着浏览器引擎的计算负载加重。
-
分析样式表的复杂性:分析样式表有助于发现有问题的,冗余和重复的 CSS 选择器。分析出 CSS 中冗余和重复 CSS 选择器,可以删除这些代码,加速 CSS 文件读取和加载。可以使用 TestMyCSS、analyze-css、Project Wallace 和 CSS Stats 来帮助你分析和更正 CSS 代码