导语 | 腾讯文档 SmartSheet 视图是多种视图中的一种,该模式下 FPS 仅 20 几帧(一般 Sheet 视图下 58 帧),用户体验十分卡顿。腾讯文档团队针对该问题进行优化,通过禁用取色、多卡片离屏渲染等形式实现 FPS 靠近 60 帧,晋升两倍多。本文将具体介绍其挑战和解决方案,并输入通用的教训办法。心愿本文对你有帮忙。
目录
1 前言
2 增量渲染
3 剖析火焰图
4 禁用取色
5 缩小搜寻后果匹配
6 防止应用 clone
7 多卡片离屏渲染
7.1 多卡片 vs 整屏
7.2 实现
8 文本缓存
9 最初
01
前言
腾讯文档智能表格是一种领有多视图的新型表格。智能表格也是一个人造的低代码平台,只有应用凋谢的增删改查 API 就能实现一个后盾管理系统,利用提供的各种视图将数据展现进去。它实质上是一个在线数据库,领有更丰盛的列类型和视图。智能表格能够让一份数据多种维度展现。目前曾经有表格视图、看板视图(SmartSheet 视图)、画册视图、甘特视图、日历视图等。
除了最被熟知的表格视图之外,SmartSheet 看板视图以卡片的模式来展示,非常适合做一些经营流动和项目管理,从而开始失去关注。看板视图能够依据单选列作为分组根据,进行卡片的一个聚合分组展现。卡片的高度是不固定的,只有当前列有内容才会展现进去。下图是腾讯文档智能表格 SmartSheet 看板视图的无封面版本和有封面版本:
SmartSheet 看板视图上线后,10 w 单元格场景下的 FPS 只有 20 多帧,比起 Sheet 视图的 58 帧差距比拟大,用户体验十分卡顿。
FPS (Frames Per Second) 就是每秒钟画面的更新次数。实践上 FPS 越高,动画就会越晦涩。因为大多数设施屏幕刷新率都是 60 次 / 秒,所以一般来说 FPS 为 60 帧的时候最晦涩,此时每帧的耗费工夫约为 16.67 ms。如果 FPS 低于 30 帧,就会呈现显著的卡顿和不晦涩。所以腾讯文档团队优化的重点指标是:尽量将每一帧的耗时升高到 16.67 ms。
02
增量渲染
Smart Sheet 看板是多种视图中的一种。它次要是多个分组来组成的,每个分组又包含了多个卡片。滚动的时候包含左右分组滚动、分组内卡片高低滚动两种。
先来理解渲染层的实现,Smart Sheet 看板渲染层初始化分为 4 个阶段:
- 第一阶段,收集计算文本宽高、截断等等;
- 第二阶段,收集各种树形构造的 widget,比方 textPainter、cardPainter、groupPainter 等等。
- 第三阶段,基于 widget 进行绘制,从根 layoutTree 开始递归子节点执行 painter 办法;
- 第四阶段,Konva 执行 Layer 的 batchDraw 办法,递归执行子节点的 draw 办法。
10 w 单元格不会将全副卡片都给绘制进去。因为它一方面会导致绘制工夫过长,另一方面寄存绘制信息占用的内存太多。
所以只会收集可视区域内的 widget 进行绘制。在滚动的时候,会计算出须要销毁的卡片和须要新增的卡片,而后开始销毁后面的节点,从新创立新的节点,进行增量渲染。对应下面的第 2、3 步,但此时只会收集增量的 Painter。
03
剖析火焰图
首先须要晓得滚动的时候次要是耗时在哪里。关上 Chrome 的 Performance 选项,抉择最右边的实心圆录制,在页面上用鼠标滚动。最初生成了上面这份火焰图,能够看到有很多红色倒三角,阐明这里呈现了一些很耗时的操作。
放大这个火焰图,能够看到其中的一个 Task 的耗时,也就是一帧的耗时。能够看到两种状况,后者显著比前者耗时多太多了。
- Task1:
-
Task2:
那滚动的时候渲染层做了哪些事件呢?次要是上面几步:
- 第一步,对原来的分组设置偏移量;
- 第二步,计算新的可视区域,包含须要销毁、创立的分组和卡片;
- 第三步,收集分组或者卡片的 widget;
- 第四步,基于 widget 进行绘制,次要是创立 Konva 节点,增加子节点等;
- 第五步,触发 Layer 的 batchDraw 办法,遍历子节点进行绘制。
04
禁用取色
能够从下面看到 getImageData 耗时十分多,那 为什么滚动的时候会用到 getImageData 呢?这就不得不说到 Canvas 的事件零碎了。
Canvas 不像 DOM 一样领有事件零碎,所以无奈间接晓得以后点击的是哪个图形,须要开发者本人实现一套事件零碎。简略来说,就是晓得某个坐标点以后对应的是什么图形。
Konva 为了可能依据坐标点匹配到触发的元素,采纳了 色值法——也就是在内存外面的 hitCanvas 外面绘制截然不同的图形,给这个图形加一个随机填充色,生成一个 colorKey。而后以这个 colorKey 作为 key,Shape 作为 value,存了起来。
事件触发时通过 hitCanvas 的 getImageData 办法拿到 colorKey,进一步拿到对应的 Shape。
咱们在本人电脑本地执行了 1000 次 getImageData,发现耗时十分多。在滚动的时候,很容易触发大量调用 getImageData。
Navigated to file xx
getImageData 1000 次: 250.051025390625 ms
Navigated to file xx
getImageData 1000 次: 245.02587890625 ms
Navigated to file xx
getImageData 1000 次: 245.637939453125 ms
Navigated to file xx
getImageData 1000 次: 254.847900390625 ms
怎么防止调用 getImageData 呢?咱们来翻翻 Konva 的源码。
滚动的时候,触发的是 wheel 事件。只须要在滚动的时候设置 layer 的 isListening 为 false 即可。等滚动完结后再设置回来,所以这里是 debounce 的逻辑。
05
缩小搜寻后果匹配
后面咱们说过,渲染层在渲染的时候会进行收集,在滚动的时候因为可能会有搜寻后果高亮的存在,所以也要计算 以后卡片是否匹配搜寻后果。如果匹配了,那就设置背景色。
但如果在没有启动搜寻的时候,不应该遍历 layoutTree,而是应该间接返回。提前返回,能够节俭大概 2 ms 的搜寻高亮收集工夫。
06
防止应用 clone
很多文本和矩形有独特属性,所以咱们本来是先创立了一个节点,应用的时候通过 clone 的形式复用,而后用 setAttrs 来设置新的 config。
但 clone 的实现比较复杂。能够了解成进行了一次深拷贝,会带来一些性能损耗。
这里不够优雅,能够提前缓存通用的 config 值,而后间接应用 new 来创立节点。
从图上能够看到,很显著耗时降落了。
当咱们优化到这一步发现:在没有呈现新的卡片时,滚动的耗时曾经非常少了,基本上耗时都在绘制阶段。
绘制阶段的耗时达到了 13 ms 之多。
07
多卡片离屏渲染
绘制阶段要怎么去优化耗时呢?页面滚动的时候,每次其实只挪动了一小段距离,只有这部分是新增的。那也就意味着后面大部分都是不变的,只是减少了一些偏移量,如果可能对其进行复用,那必定能够大大减少耗时。
离屏渲染 是 Canvas 的一种广泛的优化伎俩。比方腾讯文档团队的 Sheet 和 Word 都有离屏渲染,思路都是在滚动的时候,通过 drawImage 来复用后面曾经绘制的局部,而后再绘制增量的局部,这样能够缩小大量文本的绘制。
7.1 多卡片 vs 整屏
Smart Sheet 相比 Sheet 和 Word 来说会非凡一些,腾讯文档团队应用了 Konva 这个框架,它本身封装了一套渲染逻辑,所以对于 Word 这种离屏渲染来说,实现起来比拟麻烦。
因而,针对看板的状况,能够针对多个卡片做离屏渲染。多个卡片离屏渲染比整屏离屏渲染更有劣势。
看板滚动次要有两种状况:
- 第一种,没有呈现新的分组和卡片,以后只是在可视区域的卡片内滚动;
- 第二种,呈现了新的分组和卡片,波及到了节点的销毁和新增。
对于第一种状况来说,此时没有新增卡片,多卡片离屏渲染只须要把离屏 Canvas 外面的内容绘制到主屏就行了。但整屏离屏渲染仍然会去多渲染增量局部,因为它是以整个屏幕为纬度的;对于第二种状况来说,两者都须要绘制增量局部的卡片,所以实践上耗费是一样的。
但在疾速滚动的状况下,大部分工夫都是没有呈现新的分组的,大概率是在可视区内的几个分组挪动,所以这种状况下,如果应用整屏渲染,就不得不多去渲染一个分组。
7.2 实现
在创立 Group 的时候,减少一个 offscreen 选项,它会多创立一个离屏 Canvas。也就是 offscreenCanvas,这个 canvas 会依据主屏的 Group 外面的子元素来先绘制一遍。
在 Group 的理论绘制办法 drawScene 办法外面,判断以后 Group 是否存在离屏 Canvas。如果存在离屏 Canvas,那就间接用 drawImage 的形式。
那离屏的 Canvas 什么时候生效呢?因为看板的特殊性,用户批改了某个单元格有可能造成宽低等信息的变动。所以不得不从新计算一遍,这个时候也会从新绘制。
之前的节点都会被销毁掉,而后创立新的节点。因而这个时候从新创立了新的离屏 Canvas 就不会生效了。滚动的时候同理,滚出屏幕外的节点被销毁了,新增的节点从新创立了离屏 Canvas。各位开发者能够看到最终的优化成果,绘制的耗时只有 2 ms。
但正如后面说的,离屏渲染只是针对曾经渲染好的卡片进行的。那如果滚动的时候,呈现了新的卡片怎么办?这部分渲染仍然会很耗时。
08
文本缓存
绘制可复用的局部解决完了,然而 绘制增量 的局部耗时仍然很高,常常能够达到 20 ms。因为它须要先收集 painter,而后去绘制 widget。收集局部耗时曾经优化到很低了,但绘制局部耗时仍然很高。那要怎么解决呢?
如果是在文本量不多的时候,这部分耗时曾经非常低,每帧耗时降至 58 ms,但文本量大的时候耗时就增多了。从图上能够发现,耗时次要产生在文本的计算和绘制下面。那文本计算了哪些呢?
- 第一,如果给定文本宽度,那文本须要在哪个字符进行截断、换行;
- 第二,文本最初一行的前面是否须要增加省略号。
文本换行和截断,在 Konva 外面进行了非常复杂的计算。次要是对文本进行二分查找,顺次找到最终须要截断的字符地位。如果有换行符,须要对换行符进行非凡解决。如果传入的截断形式是 ‘word’,那还须要对空格和 - 进行特地的解决。如果传入的是 ellipsis,那须要在最初一行减少省略号。
这些简单的计算自身会耗费一些工夫,其中通过二分查找也会大量调用 measureText 办法。那要怎么解决呢?看板因为须要记录用户上次关上滚动条的地位,再次关上的时候须要跳转过来。为了防止滚动的时候,再去实时计算以后应该新增或缩小哪些卡片,会在最开始的时候一次性计算好所有的卡片宽高。
卡片宽度波及到文本、图片等宽高,也就是说最开始曾经解决过文本计算,那这部分缓存起来不就好了?所以在最开始计算的时候能够把属性为 key、宽低等信息作为 value 一起存入 cacheText 外面,而后在 setTextData 外面判断 cacheText 外面是否有缓存,如果有的话就不须要从新计算一遍了。
这里缓存了三个信息,别离是 文本宽度、文本高度、文本子串数组(被截断分成了好几个)。
但这样还是会有一些问题:如果文本特地长的话,那 textArr 也会比拟大,容易导致内存增长。咱们批改策略:不存 textArr,而是存每个子串完结的 index 值(换行的 index 值)。
另外,在最开始计算的时候,只是为了算出文本的高度,绘制阶段最多只展现 4 行,超过 4 行就须要增加省略号,所以算出高度后还要判断是否超过了 4 行。如果间接用最开始计算的后果,它可能包含了超过 4 行的信息,导致绘制阶段不精确。例如存了六行,那绘制的时候须要绘制前 4 行;然而省略号是在第六行,导致在第 4 行失落了省略号。
因而须要基于业务进一步深度定制,针对 Text 进行一次封装。为了防止动到计算换行的逻辑,咱们减少了一个标记位,用于判断以后传入的 height 示意最大高度。
09
总结与思考
腾讯文档团队优化后的 FPS 靠近 60 帧,从 20 多帧晋升到 58 帧左右,也就是晋升了两倍多。
在这期间,团队总结了相干教训:应该尽量避免滚动的时候有阻塞主线程的耗时操作。很多中央不易被发现,如深拷贝、序列化、反序列化等等。一些简单又耗时的计算能够将计算工作的后果提前缓存起来,这样滚动的时候就能够间接从缓存外面读取了。因为这里本来就须要在加载的时候去计算这些,所以就进行了一些革新,让其反对缓存。
如果想不拖慢首屏渲染速度,还能够放到 Web Worker 外面去计算,比方多计算几个分组的文本信息。针对一些比拟耗时的绘制操作能够应用离屏渲染的模式来防止反复绘制。这里还能够思考应用原生的 Offscreen 配合 Web Worker 来施展离屏渲染的劣势。以上是本次分享全部内容,欢送各位开发者在评论区分享交换。
你可能感兴趣的腾讯工程师作品
|微信全文搜寻耗时降 94%?咱们用了这种计划
|腾讯工程师聊 ChatGPT 技术「文集」
|腾讯云开发者热门技术干货汇总
|一文读懂 Redis 架构演变之路
技术盲盒:前端|后端|AI 与算法|运维|工程师文化