复杂渲染引擎架构与设计--6.增量渲染
更新日期:
对于渲染引擎来说,如果每次都进行完整内容的计算和绘制,在低端机器或是负责页面的时候可能会出现卡顿。
因此,我们可以考虑设计一套增量渲染的能力,来实现改多少、重绘多少,减少每次渲染的耗时,提升用户的体验。
增量渲染设计
所谓增量渲染,或许你已经从 React/Vue 等框架中有所耳闻,即更新仅需要更新的部分内容,而不是每次都重新计算和渲染。
React 增量渲染
React 里结合了虚拟 DOM 以及 Fiber 引擎来实现完整的 Diff 计算和渲染调度,这些我之前在其他文章也有说过。在 React 里,状态的更新机制主要由两个步骤组成:
- 找出变化的组件,每当有更新发生时,协调器会做如下工作:
- 调用组件 render 方法将 JSX 转化为虚拟 DOM
- 进行虚拟 DOM Diff 并找出变化的虚拟 DO
- 通知渲染器。渲染器接到协调器通知,将变化的组件渲染到页面上。
我们的渲染引擎道理也是十分相似的,即找出最小变化范围进行计算和更新。同样的,我们还是继续以在线表格为例子,基于我们现有的引擎设计上实现增量渲染。
收集增量
关于渲染引擎的收集和渲染过程,已经在前面《1.收集与渲染》文章中介绍过。
基于该架构设计,我们知道一次渲染分成两个过程:
- 收集渲染数据。
- 绘制收集后的渲染数据。
而前面在《2.插件的实现》中也提到,渲染引擎整体的架构如图:
在该架构图中,渲染引擎支持提供绘制特定范围的能力。而要实现这样的能力,我们需要做到:
- 支持特定范围的渲染数据收集。
- 支持特定范围的 Canvas 画布重绘。
由于在线表格这样的产品都是以单元格为基础,因此我们的收集器和渲染器都同样可以以单元格为最小单位,提供以下的能力:
- 根据格子位置更新、清理、新增收集的渲染数据。
- 根据格子位置进行画布的擦除和重新绘制。
实际上,一次渲染的耗时大头更多会出现在收集过程,因为收集过程中常常会进行较复杂的计算,亦或是针对一个个格子的数据收集会导致不停地遍历各个格子范围和访问特定对象获取数据。
所以更重要的增量能力在于收集过程的增量。
在线表格增量渲染
对于在线表格的场景,我们可以考虑两种增量渲染的情况:
- 局部修改,比如用户修改了某个范围的格子内容和样式。
- 页面滚动,用户滚动过程中,有部分单元格范围不变。
我们分别来看看。
局部修改
局部修改比较简单,前面我们已经提到说收集过程和渲染过程都支持按指定范围进行增量渲染,因此局部修改的时候直接走特定范围的绘制即可。比如用户修改了 A1 这个格子:
- 清除 A1 单元格的收集数据。
- 重新收集 A1 单元格的收集数据。
- 重新渲染 A1 单元格的内容。
页面滚动
页面滚动与纯某个特定范围的修改不大一样,因为页面滚动过程中,所有单元格的位置都会发生改变。
一般来说,在滚动过程我们会产生局部可复用的单元格绘制结果,如图:
对于这样的情况,我们可以有两种解决方案:
- 复用局部 Canvas 绘制结果。
- 复用局部收集的渲染数据结构。
方案 1 直接复用局部 Canvas 的方案比较简单,不少在线表格像谷歌表格、飞书文档等都是用的该方案,该方案同样存在一些问题:
- 由于 Canvas 绘制在非整数像素下会存在不准确的问题,因此在有缩放比例下增量渲染会出现多余的横线、白线等问题
- 由于该过程会将原有 Canvas 的内容先转成图片,再往新的内容区域贴进去,会导致 Canvas 透明度丢失,无法支持 Canvas 的透明设置
方案 2 复用局部收集的渲染数据结构,可以优化上述问题,但整体的性能会比复用 Canvas 稍微差一些,毕竟复用 Canvas 直接节省了复用范围的收集和渲染耗时,而复用收集结果则仅节省了复用范围的收集,绘制过程还是会全量绘制。
对于复用收集结果的方案,还需要考虑页面出现滚动导致的绘制位置差异,即使是同一个单元格,其在画布上的位置也发生了改变,这样的变化需要考虑进去。
因此,收集器的数据结构需要和单元格紧密相关,而不是基于 Canvas 的整体偏移。如何设计出性能较好又易于理解的数据结构,这也是一项不小的挑战,决定了我们增量渲染的优化效果能到哪里。
结束语
由于渲染引擎和用户视觉、交互紧密相关,因此常常是性能优化的大头。结合产品特点和架构设计做具体的分析和优化,这才是我们在实际工作中常常面临的挑战。
前面介绍过的分片优化也好,这里的增量渲染也好,其实大多数都能在业界找到类似的思路来做参考。不要把思路局限在相同产品、相同场景下的解决方案,即使是看似毫不相干的优化场景,你也能拓展思维看看对自己遇到的难题是否能有所启发。
码生艰难,写文不易,给我家猪囤点猫粮了喵~
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢