前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >腾讯文档Doc Canvas渲染引擎流程改造

腾讯文档Doc Canvas渲染引擎流程改造

原创
作者头像
jayson阿骏
发布2022-11-22 13:09:13
4.4K0
发布2022-11-22 13:09:13
举报
文章被收录于专栏:web前端渲染web前端渲染

为了解决部分历史渲染问题,实现移动端canvas渲染的新功能,以及支持后续功能扩展,对腾讯文档Doc Canvas渲染引擎的流程进行了改造,本文对改造进行介绍和小结。

1. 改造背景

1.1. 解决历史问题

Doc文档滚动过程中偶现渲染空白(safari浏览器出现频率较高):

图1 滚动渲染空白BUG
图1 滚动渲染空白BUG

1.2. 实现新功能(移动端canvas引擎统一渲染)

为了支持在移动端预览和PC端完全一致的文档内容(更完整排版、格式支持),需要在移动端通过canvas渲染引擎统一进行渲染;然而直接移植复用canvas渲染,原有渲染引擎在移动端存在性能问题,例如滚动存在明显卡顿(平均FPS低于15):

图2 移动端canvas渲染滚动卡顿
图2 移动端canvas渲染滚动卡顿

1.3. 支持后续功能扩展

后续浮动环绕文本框、图形等内容,可能拥有多个嵌套层级,且每个浮动元素有独立的overlay (高亮、底色)层级,例如下图的多个浮动文本框内容:

图3 Microsoft word 多层浮动文本框示例
图3 Microsoft word 多层浮动文本框示例

原有canvas渲染引擎直接复用,还原渲染上图内容的效果如下图所示:

图4 原渲染引擎还原效果
图4 原渲染引擎还原效果

所以,为了解决上述问题,需要对原有canvas渲染引擎进行改造优化。

2. 渲染流程剖析

2.1. 渲染层基本流程介绍

渲染层(Render Engine)最基本的能力就是将上层排版层生成的文档视图树形结构LayoutBox进行收集和渲染,最终将文档视图呈现在屏幕上,示意图如下图所示:

图5 渲染层基本能力示意图
图5 渲染层基本能力示意图

而要详细说明渲染层的收集和渲染流程,需要先简单介绍LayoutBox,如下图所示,LayoutBox是腾讯文档Doc经过排版后生成的用于描述文档页面信息的树形结构,不同类型的box表示文档中不同的层级和内容:

图6 LayoutBox示意图
图6 LayoutBox示意图

渲染层收集的目的,就是通过可视区域等信息判断并计算出需要渲染的文档区域,然后根据需要渲染的区域遍历LayoutBox树进行剪枝并收集需要渲染的box节点,最后对收集的结果按照层级进行排序以便后续渲染。剪枝示意图如下图所示:

图7 收集-剪枝示意图
图7 收集-剪枝示意图

渲染收集的剪枝旨在精确缩小需要渲染的内容范围,减少多余部分的遍历和渲染,降低多余的开销;收集过程中对收集的结果按照视图类型和渲染优先级进行排序,除了满足渲染优先级以外,同样也是为了减少渲染过程中canvas状态机的切换从而降低渲染开销、提升性能。渲染层收集、渲染核心流程示意图,如下图所示:

图8 渲染层收集、渲染核心流程
图8 渲染层收集、渲染核心流程

2.2. 不同场景渲染流程分析

介绍完渲染层基础流程,接下来针对不同场景的渲染流程进行介绍,以及针对改造背景中的问题进行对应分析。

2.1 滚动场景渲染

2.1.1 滚动场景渲染流程

如下图9所示,滚动场景下针对可重用的文档区域(滚动到下一帧渲染时还在可视范围的区域),为了避免多余的基础渲染流程(收集+渲染),直接使用canvas 基础 API drawImage将对应区域直接绘制到离屏canvas(在内存中创建的canvas元素,未dom挂载在页面上展示);针对新渲染区域(滚动产生的新出现在可视范围的区域),则在离屏canvas中执行基础渲染,并将对应区域drawImage绘回主canvas(展示文档内容的canvas)。

图9 滚动场景渲染流程示意图
图9 滚动场景渲染流程示意图
2.1.2 离屏canvas drawImage三宗罪

上述滚动场景的渲染流程,本意是通过drawImage将已经渲染的canvas画布当作图片来复用,从而节省掉多余的基础渲染流程:不需要重复执行遍历layoutBox 树进行裁剪以及后续收集、排序、绘制等开销比较高的逻辑。原本流程本身没有问题,且将canvas的常规性能优化手段都应用上了。

然而,问题就出在不同的浏览器以及系统平台对于canvas的支持度和兼容情况不尽相同,这里根据上述改造背景中的部分问题主要总结离屏canvas drawImage的三宗罪

  • iOS移动端存在canvas画布尺寸以及显存限制

实际上各浏览器对canvas画布最大尺寸都会有限制(超过限制canvas的渲染将会失效):

(来源:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)
(来源:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)

一般而言应用中的canvas尺寸都不会超过上述限制,可以正常使用,然而在移动端iOS/safari canvas的尺寸限制会小很多:

除了canvas尺寸限制,甚至还有canvas画布占用的显存限制:

所以对于iOS移动端,canvas的使用需要非常谨慎,尽可能减少canvas的数量和尺寸,避免超过限制引发BUG。然而drawImage的使用,依赖额外的离屏canvas,这样相当于直接把canvas的数量乘以了2倍。

  • safari浏览器对drawImage限制,导致渲染白屏

此问题主要集中在safari浏览器,正常滚动文档页面会偶现canvas drawImage不生效导致渲染白屏的问题。

由上述(1)可知,当canvas画布尺寸超过浏览器限制时,会导致canvas绘制失效,safari会在控制台弹出警告:

chrome和safari绘制失败的canvas画布尺寸上限比较一致,但chrome会直接绘制失效,没有任何提示。可以使用试验demo验证: https://xdevilj136.github.io//large_canvas_drawImage_bug.html

canvas画布尺寸超过限制 ,drawImage失效demo演示 width(27000px)* height(10000px)
canvas画布尺寸超过限制 ,drawImage失效demo演示 width(27000px)* height(10000px)

然而Doc文档页面的canvas尺寸乘积远远没有达到这个级别,非放大情况下大概width(3600px)*height(1600px) = 5760000,缩放到最大400%时大概23040000,而且在safari复现问题时也并未弹出警告或提示。

由于safari浏览器内核逻辑对开发者来说是个黑盒,所以只能进行对照实验:

  1. 去掉渲染复用逻辑——去掉drawImage调用,全屏重新渲染,渲染空白的问题不再出现(当然全屏重新渲染会影响性能)
  2. 进行对比实验发现增加canvas画布尺寸时,drawImage的失败概率会大大增加导致渲染出现空白(width:4600px ,height:1600px时,失败概率50%以上)

对照实验结果说明渲染空白问题确实和drawImage相关,且在canvas画布尺寸大到一定量级时,浏览器有相应的逻辑限制drawImage绘制。

  • 移动端下drawImage开销巨大

针对移动端渲染性能问题,经过分析发现虽然在PC端drawImage的开销基本忽略不计,但在移动端(Android和iOS)下开销巨大,甚至高于对可重用区域进行重新收集、渲染的开销。

PC端滚动渲染performance:

Android移动端滚动渲染performance:

由上图对比可以看出,在移动端单次drawImage开销就高达15ms,在单次渲染task中的开销占比非常高,是造成移动端下canvas渲染引擎性能问题的罪魁祸首之一。

2.1.3 canvas分层雪上加霜

渲染层针对不同渲染场景,为了避免无效重绘,提升渲染效能,对不同的渲染内容做了分层。每层渲染拥有独立性,减小重绘粒度,降低了层级间的干扰:

图10 渲染分层架构
图10 渲染分层架构

其中canvas也做了分层,将文档主内容和overlay(选区、高亮、底色)分为两个canvas层分别进行渲染;主要针对仅切换选区或底色等内容时,可只处理overlay层的渲染,无须重复渲染main canvas (文档主内容)。

canvas的分层旨在进一步提升部分场景的渲染性能,然而经过上述2.1.2的分析后发现,canvas分层增加了canvas的数量导致:drawImage的调用频次增加、canvas占用的显存增加(每一层canvas对应一个额外的离屏canvas,让问题更加突出)。

注:另外canvas的分层还导致后续需要支持的浮动元素(文本框、图形)渲染受限,浮动元素拥有多层嵌套层级,且每个元素拥有单独的overlay(高亮、底色、选区),如果将overlay和主内容分层,则无法按照正常层级顺序渲染:

图11 canvas分层浮动元素层级示意图
图11 canvas分层浮动元素层级示意图

overlay和主内容main canvas,两个独立的canvas画布拥有不同的层级上下文,尽管在canvas内部可以管理不同的层级,但overlay和main canvas始终只能被另一方覆盖:

图12 canvas分层导致两个canvas互相覆盖
图12 canvas分层导致两个canvas互相覆盖

2.2 编辑场景渲染

2.2.1 编辑场景渲染流程

如图13所示,在编辑文档时,无论编辑的内容范围多大,渲染层都会将整个可视区域+buffer区域(可视区域上下缓冲区域) 作为脏区(需要重新渲染的区域),根据脏区对整个文档的排版DocumentBox进行遍历裁剪并将整个脏区对应的内容进行收集和重新渲染。

图13 编辑场景渲染流程示意图
图13 编辑场景渲染流程示意图
2.2.2 脏区范围大

对于编辑渲染流程,比较直观的感受便是渲染脏区范围较大,因为在编辑场景渲染层仅仅监听排版变化的layoutChange事件来进行重新渲染,故只能通过可视区域来判断并计算脏区。另外,渲染层仅仅使用两个canvas画布(主内容和overlay)对整个文档进行渲染展示,canvas画布尺寸和脏区大小一一对应,而canvas画布尺寸和canvas渲染耗时是正相关的:

图14 canvas渲染耗时和渲染尺寸相关趋势图 (来源:https://smus.com/canvas-vs-svg-performance/)
图14 canvas渲染耗时和渲染尺寸相关趋势图 (来源:https://smus.com/canvas-vs-svg-performance/)

所以渲染脏区越大,渲染开销越高,性能越差。主要体验在两方面:

  1. canvas画布尺寸大,渲染耗时高
  2. 渲染的内容多,遍历收集开销更高,特别对于一些嵌套层级可能较深的LayoutBox(如:表格)影响会更大

3. 分页渲染流程改造方案

3.1 滚动场景去掉离屏渲染(drawImage)

通过上述分析,渲染流程上去掉canvas drawImage是比较迫切的需求,而drawImage的调用主要应用在滚动场景的离屏渲染,其作用就是为了尽可能复用渲染内容减少重新渲染。

那么是否有方案可以不使用离屏渲染(drawImage),同时又能复用渲染内容呢?

想到移动端常用的虚拟列表优化方案,可以用来优化长列表滚动性能:

图15 虚拟列表优化方案
图15 虚拟列表优化方案

虚拟列表通过缓存列表数据,每次仅渲染可视区域对应的item dom节点,上下滚动时可复用dom节点仅更新dom对应的数据或样式,既避免dom数量过多,又减少了销毁和重新创建dom的开销。

Doc文档的滚动实际非常类似,且分页模式下排版结构中分页LogicPage和item可以天然对应起来:

图16 滚动场景分页渲染
图16 滚动场景分页渲染

分页渲染将每次渲染和复用的最小单位固定为文档的分页(对应排版结构LogicPage),滚动过程中仅仅需要对出现在渲染区域的新分页进行渲染,且新渲染分页可以复用脱离渲染区域的分页DOM,未脱离渲染区域的分页则无需任何更新。

通过这样的流程改造后,有以下收益:

  1. 可以完全弃用离屏canvas和drawImage,解决了drawImage带来的问题,减少了离屏canvas带来的额外显存和总画布尺寸占用
  2. 一个分页对应一个canvas, 减少了单个canvas的尺寸,一定程度上提升了渲染性能

然而以上流程仅仅适用于分页模式,流式模式下整个Doc文档的排版结构只有一个LogicPage(只有一页),为了解决流式模式仍然存在的以上问题且让渲染流程统一,接下来选择对排版层动手:

图17 流式模式排版虚拟分页
图17 流式模式排版虚拟分页

如上图所示,对流式模式下的排版进行了调整,将原先整个文档仅有一个分页LogicPage的排版结构,拆分为多个LogicPage,一个LogicPage对应一个虚拟分页。至此,流式模式和分页模式的分页渲染流程完全统一起来。

3.2 编辑场景减少脏区范围

解决完滚动场景下渲染问题,还需要考虑编辑场景。由上述2.2分析可知,原先渲染流程针对编辑场景,是将整个可视区域+buffer视为脏区进行了重新的收集和渲染,渲染脏区范围大。造成这个结果的原因主要是原先渲染层受限于以下两点:

  1. 流式模式下仅一个分页,编辑更新文档无法通过排版层精确获取脏区范围
  2. 分页模式下,虽然能通过排版层精确获取脏区对应的分页范围,但渲染上使用单独的canvas(不考虑分层和离屏)对整屏进行渲染,仍然需要对整个文档剪枝、收集

分页渲染则解决了这些限制,将编辑场景的渲染脏区减少为分页范围:

图18 编辑场景分页渲染
图18 编辑场景分页渲染

由上图示意,得益于流式模式下的虚拟分页,编辑场景下的脏区范围减少为分页范围,不在脏区的其他分页则可以完全复用,分页模式下也是同理。

注:编辑场景下,也可能出现编辑大范围内容并覆盖了多个分页的情况,这种情况下脏区最大范围也仅仅是可视区域对应的所有分页

3.3 增加canvas回收机制

经过以上改造,分页渲染的基本框架已经确定,但仍然有一些特殊情况需要考虑:

  1. 流式模式下的虚拟分页,排版层暂时还无法处理长图、长表格等内容的拆分,导致存在这些特殊内容排版结果会存在特别长的虚拟分页,进一步导致单个canvas画布特别大且对应渲染范围过大,严重影响渲染性能
  2. 放大页面,可视区域覆盖的分页数量减少,此时为了尽可能dom复用,可以保留不在可视区域的分页视图dom;但会导致放大后的分页对应canvas画布过大(如上述2.1.2的描述,在iOS移动端过大的canvas画布会因为尺寸和显存限制导致canvas渲染失效)

所以,针对以上特殊情况,渲染层增加了canvas回收机制:

  1. 首先对超长的虚拟分页对应的canvas,在渲染层拆分成更细粒度的二级canvas
  2. 对脱离可视区域的canvas, 进行画布回收

canvas回收机制示意图如下:

图19 超长虚拟分页canvas拆分
图19 超长虚拟分页canvas拆分
图20 放大页面回收canvas
图20 放大页面回收canvas

其中,对canvas的回收仅仅回收canvas画布,并不对canvas dom进行销毁,避免重新渲染时

增加新建dom开销, 回收逻辑如下:

代码语言:javascript
复制
canvasElement.width = 1;
canvasElement.height = 1;

直接将canvas画布width和height属性置为1,既能清空canvas绘制内容也能回收掉canvas画布占用的显存。

但……为什么不直接将width和height设置为0呢?

可以看下两种回收设置对比:

width = 0, height=0
width = 0, height=0
width = 1, height=1
width = 1, height=1

如上图所示,在safari浏览器,直接将canvas画布设置为width = 0, height=0,虽然画布尺寸确实更新为0,但是占用的显存并没有被浏览器回收。

(注:设置width和height为0进行回收的方式,在chrome可以正常回收显存;且在safari进行测试也是能正常回收,但safari devtools显示内存一直占用,此点尚且存疑)

增加canvas回收机制后,canvas画布所占尺寸和显存前后对比,canvas占用显存和尺寸均下降40%左右,如下图所示:

图21 增加canvas回收机制前后对比
图21 增加canvas回收机制前后对比

3.4 合并canvas,渲染层级统一管理

由上述2.1.3分析,还存在canvas分层带来的部分问题,main canvas和overlay canvas分层导致canvas画布数量翻倍,且渲染层级的管理无法支持后续扩展功能。

canvas分层目的主要针对切换选区或底色等内容时,可只处理overlay层的渲染,无须重复渲染main canvas (文档主内容),从而提升以上场景时的渲染性能。然而经过分析发现,渲染的开销主要集中在遍历、收集阶段,而非绘制阶段:

图22 单次渲染开销耗时
图22 单次渲染开销耗时

而canvas分层优化的开销主要是绘制阶段,遍历和收集的开销变化不大;另外,经过分页渲染流程改造后,单次渲染的区域减少进一步降低了绘制的开销。

再者,考虑到要支持环绕浮动元素的层级渲染,将选区、底色等和文档主内容放到同一个canvas层统一进行层级的管理是首选。所以对canvas层级进行合并:

图23 合并canvas示意图
图23 合并canvas示意图

文档主内容和overlay(高亮、底色、选区)全部合并到同一个canvas来进行渲染,不同内容层级可以统一管理,改造后,最终还原多个层级浮动文本框效果如下:

图24 合并canvas后还原浮动文本框效果
图24 合并canvas后还原浮动文本框效果

4. 总结

经过分页渲染改造,解决了滚动时渲染空白的历史问题,对后续环绕元素的层级渲染提供了支持;最重要的是解决了canvas渲染引擎在移动端的性能问题,使移动端的“分页视图”新功能可以正常使用,让用户可以直接在移动端浏览到和PC端渲染完全一致的Doc文档。

移动端滚动场景优化前后对比:

图25 移动端canvas渲染滚动场景优化前后对比
图25 移动端canvas渲染滚动场景优化前后对比

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 改造背景
    • 1.1. 解决历史问题
      • 1.2. 实现新功能(移动端canvas引擎统一渲染)
        • 1.3. 支持后续功能扩展
        • 2. 渲染流程剖析
          • 2.1. 渲染层基本流程介绍
            • 2.2. 不同场景渲染流程分析
              • 2.1 滚动场景渲染
              • 2.2 编辑场景渲染
          • 3. 分页渲染流程改造方案
            • 3.1 滚动场景去掉离屏渲染(drawImage)
              • 3.2 编辑场景减少脏区范围
                • 3.3 增加canvas回收机制
                  • 3.4 合并canvas,渲染层级统一管理
                  • 4. 总结
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档