[关闭]
@gyyin 2022-02-16T20:52:35.000000Z 字数 5582 阅读 287

组织话术

答辩


  1. 各位评委老师大家好,我是 pcg 社交协作产品部表格编辑四组的 gloryyin。我今天答辩的目标职级是 t9。
  2. 今天我的答辩主要围绕个人介绍、优化成果和技术影响力的这三个方面来展开。
  3. 首先我来进行一下个人介绍。
  4. 我是2017年本科毕业的,去年4月份加入了腾讯文档这边。
  5. 目前我在腾讯文档这边主要做smartsheet的项目。smartsheet是比sheet展现更丰富的超级表格。它拥有表格、看板、画册、甘特图等多种视图。如图所示,它支持更加丰富的列类型展示。对于看板视图来说,它是一个个卡片组成的,每个卡片都是一条数据。所以在一些项目管理和运营活动中非常适用。
  6. smartsheet还支持分组,允许我们以一定的条件聚合数据,可以看图上按照优先级进行分组,看板里面每一列都是一个分组。
  7. 我在这边主要做的是看板和画册canvas渲染层开发和性能优化以及服务端渲染的开发,关于渲染层详细的架构设计可以看附录2

  8. 我这边的第一个优化成果就是看板滚动优化。

  9. 我在开发中发现在大数据量的情况下,渲染性能低下,fps和渲染表现比较差。因为没有针对首屏可视区进行处理,导致渲染内容过多。
  10. 所以在这里我做了一个优化,那就是对可视区域进行渲染,对增量节点进行销毁和创建。可以看左边的图,假设这是我们的看板视图,每个方块都是一个卡片,红框是可视区域。那么当我们从红框移动到蓝框的时候,通过二分查找来计算出新的可视区域卡片。红色的节点需要销毁,绿色的节点需要创建,中间重复的节点可以用享元来复用。每个节点都是一个对象,所以这样可以降低内存占用,同时减少绘制区域。
  11. 我们优化的成果也比较明显,主要体现在收集数据和渲染可视区域,提升非常大。但虽然减少了绘制内容,但fps表现依然不理想,只有30左右。
  12. 那什么是fps呢?相信大家都比较了解,这里不多解释。总之我们需要降低每帧执行内容的耗时。
  13. 既然要降低执行内容耗时,那就需要知道哪些内容比较耗时。我录了一下滚动时的火焰图,可以看到在js脚本执行阶段耗时非常久。进一步进行分析,发现耗时点体现在这几个地方。主要分为三类:1. 绘制耗时 2. 计算耗时 3. 调用了一些耗时的 api,那我就需要针对这三个点进行一一击破。
  14. 我们做的第一个优化就是绘制方面的。可以看到每一屏内容都需要去绘制图片、文本、标签等等,尤其是绘制文本,这些 canvas 的 api 耗时太高了。那么怎么去进行优化呢?每帧滚动的时候只有一小段距离,既然这样,那是不是可以做一些复用呢?可以看图,我们将屏幕往右滚,绿色和红色相交的部分是一样的,所以这部分应该去进行复用。最初我想到通过 drawImage 的形式去绘制文本,但发现效果并不好。最后思考了一下,决定用离屏渲染的形式。
  15. 那什么是离屏渲染呢?离屏渲染就是在主屏幕有一个canvas,屏幕之外(可能在内存里面)有一个离屏的canvas,两个canvas来回交替绘制,本质上是一个类似 react fiber 的双缓冲的结构。假设页面往下滚动,可以看到离屏 canvas 红色这部分的内容是可以复用的,那么就将它当做位图绘制到主 canvas 上面,这样就避免大量调用 canvas 的绘制 api。对于新增的部分直接在主canvas上进行绘制,两者合并就是最终的显示效果。绘制完成后,还要把主canvas的内容重绘制到离屏canvas上面,滚动的时候一直重复这几个步骤。这也是竞品和 Sheet 主流的一种方案。
  16. 那么这个全屏的离屏渲染真的适用于我们的看板视图吗?这里我尝试了一下发现效果不是很好,主要有两个问题。可以看右边的滚动效果,滚动前后可视区域的节点没有发生变化,理论上应该进行一次复用。但使用全屏的离屏渲染,D1和D2依然需要重新绘制。另一个问题就是和我们现在的异步批量渲染冲突了,实现困难。什么是异步批量渲染?我们目前的 canvas 是封装的组件化形式,如果对组件修改属性,那就需要更新,但如果同时修改好几个属性,那么就会进行一次合并,到下一帧进行一次批量更新,类似 react 里面的 setState。所以离屏渲染是同步的,如果想要使用的话,那就一定要保证在下一帧之后进行离屏,否则就会被异步批量渲染给清除掉,所以这里效果不好。
  17. 基于看板的特点,就真的没办法使用离屏渲染了吗?我思考了很久,最终想到了另一种更好的方案。那就是针对每个卡片做离屏渲染,对可视区域内的卡片都创建一个离屏 canvas,这样滚动的时候如果没有出现新的卡片,那就完美复用了。如果出现了新的卡片,那就只会对新的卡片进行绘制。那么我们是怎么实现的呢?通过在卡片类上面增加一个离屏方法,在里面创建离屏的canvas,下次异步批量渲染的时候,会在绘制方法里面判断是否有离屏的canvas,如果有,那就走离屏渲染,如果没有,那就走原本的渲染。这样减少了整体的绘制,fps提高到45帧左右,针对绘制问题一步到位解决掉了。
  18. 既然绘制问题解决了,那fps依然离50帧有一些差距。这里是为什么呢?我这里定位了很久,发现主要是滚动的时候对于文本进行大量计算。可以看旁边这个图,我们给了一段话,给定一个宽度,需要计算出来在哪个字符处换行,在css里面浏览器已经帮我们做了,但在canvas里面需要我们自己去算。通过多次二分查找调用 measureText 计算出换行的地方,measureText 本身是很耗时。而且假设二分查找了10次,意味着腾讯文档这四个字被测量了10次,这里本来是可以走缓存的。
  19. 由于我们的首屏需要计算换行信息来获取卡片的高度,因为像滚动条之类的都需要拿到全量高度。所以那就可以缓存计算后的换行信息,来避免实时的文本测量。同时合并重复较多的属性作为key,通过享元模式减少内存增长。同时,对于已经计算过的词语,我们也做了缓存,避免同一个字被多次测量。
  20. 那么这里需要评估一下对内存的影响,这里随机文本了10w个单元格,发现内存增长在可以接受的范围内,实际上smartsheet的这种使用场景,不可能每个单元格的文本都不一样,所以内存消耗肯定会更少。经过这个优化,fps提高到了50帧左右。
  21. 然后就是我做的一些其他优化,比如滚动的时候禁用 getImageData,getImageData 是用色值法实现canvas事件系统需要调用的一个 api,它耗时非常大,本地跑了1000次耗时250ms。由于之前我花时间研究了很多 canvas 渲染库的源码,所以对这里比较了解,所以在滚动的时候禁用了事件监听,fps提高到了55帧。
  22. 另一个就是避免大量的深拷贝,由于项目里面很多图形都有一些共同的属性,原本写法就是先new了一个 shape,用的时候进行深拷贝,然后传入新的 config,深拷贝开销非常大。所以这里不要缓存图形,而是缓存 config,将新的 config 进行一次合并,最终进行一次 new,就降低了深拷贝的开销。
  23. 那么最后进行一次总结,针对滚动做了下面这几个优化。可视区域渲染和增量节点回收,减少绘制的内容。多卡片离屏渲染,降低了重复的绘制。将文本换行计算的结果提前进行一次缓存。减少getImageData和深拷贝的开销,这个优化过程我也已经发到了km上面,和很多其他部门的同事交流分享了一下。
  24. 最后来对比一下我们的竞品,会发现比起其他几个竞品来说,我们优势比较明显。
  25. 从28帧提高到了57帧左右,提升超过200%,且没有白屏表现,对比竞品优势明显,做到了业界领先水平。
  26. 那么讲完了fps滚动优化,我这里来分享我做的另一个首屏加载速度的优化。
  27. 我们这边目前首屏渲染耗时比较久,大概是3-4s左右,影响了用户体验。那么首屏渲染耗时在哪里呢?可以看一下首屏渲染的一个流程,主要是打开页面加载js代码、拉取首屏数据、mutation反序列化和应用、渲染层获取数据后转换成布局信息、渲染层进行canvas渲染、表格内容呈现这么几步。很明显整个流程很长,每一步耗时也比较久。
  28. 针对这几步,就需要一一进行优化。主要做了这几个优化,其中可视区域渲染前面已经讲过了,数据分片是另一位同事做的,这里不细讲了,可以参考附录3,这里主要讲一下分级加载和服务端渲染。
  29. 针对我们项目最后打包的 jsbundle 比较大的情况下,可以将资源按照优先级来拆分,从高到低依次加载,让用户首先看到核心的渲染资源,经过我们的拆分,最后资源体积从860k降低到了680k。
  30. 然后就是今天主要讲的服务端渲染,服务端渲染可以最快速度将页面展示给用户。在做服务端渲染之前,我也进行了一些竞品调研。主要调研了两种方案,其中一种就是实时直出,我们都知道服务端渲染是在服务端提前渲染好后将字符串返回给到浏览器,这样速度会比较快。但对于我们的场景来说,如果走实时的服务端渲染,那就只是将这些步骤放到服务端去做,提升有限。另外一种方案是我是从飞书文档这里调研出来的,就是服务端直出渲染层数据结构,渲染层拿到数据结构后直接进行绘制,省去了前面几步,整体上加载速度非常快。但这种方案依然要等js资源加载完成后,渲染层初始化结束才能去应用数据。
  31. 所以基于上面这些考量,我们选择了走 redis 缓存的形式,初次打开或者修改文档的时候,后台调用前端提供的云函数,传入首屏数据,获取到dom字符串后存入redis里面。下次打开时bff从后台redis获取dom字符串,返回给页面,这样相比实时直出耗时更少一些。
  32. 服务端渲染方案已经定下来,那么具体去实现的时候会有哪些难点呢?主要是怎么去实现 canvas 的服务端渲染,怎么用一套代码适配多种视图,怎么保证精确的还原。
  33. 首先来看怎么做canvas服务端渲染,这里调研了业界的几种方案,最后综合成本考虑用 dom 去模拟 canvas 来实现。
  34. 那么我们怎么去设计 dom 渲染引擎呢?先看一下渲染层的流程,在浏览器里面运行渲染层,渲染层去调用底层的渲染库,最后绘制不同的图形。提前调研了一下我们这边 sheet 的方案,sheet 是基于渲染层重写了一份提供给 Node 端运行的渲染层,本质上是一个 table 布局,最后输出 dom 字符串。但 sheet 只有一种表格视图,我们有多种视图,采用这样的方案就需要三倍人力,而且后续渲染层改动需要兼容。
  35. 我们不应该影响到渲染层正常的开发方式,也不需要去关注渲染层具体的实现,基于自身业务的形态,最终我选择替换掉我们底层的渲染库,实现一套接口一致的 dom 渲染引擎。这样还原度很高,控制更加灵活,实现也更加优雅。
  36. 那么来看一下 dom 渲染引擎具体的设计。整体是一个 dom 树的结构,底层叶子节点就是我们需要绘制的具体图形,自顶向下来收集样式和布局,最后输出 html 字符串。整体实现上也是偏向于组件化的形式。对于一些需要适应浏览器窗口宽高的情况,通过输出 js 脚本来控制执行,比较灵活。
  37. 渲染引擎设计的再好,如果还原度比较低,那就没有任何意义了。所以针对各种场景进行了精确的还原,可以看这三种情况。通过 dom 渲染标签,这里是一个 div 添加背景色。用 dom 渲染图标,这里是将项目里面的图标转成了 base64 来输出。对于三段式折线来说,很难用 dom 来进行模拟,所以这里就用了 svg 绘制折线,dom + svg 能覆盖掉 99% 的场景。
  38. 但在开发中真的有像我说的这么简单吗?比如我遇到了怎么绘制小于12px的文本,我们知道chrome浏览器里面最小字号就是12px,但项目里面用了10px的字号,所以这里也使用了 svg 来绘制,它的精确度高,实现也简单,dom 缩放将盒模型也一起缩放了,导致很难对齐。
  39. 我在项目里面遇到的另一个比较难的点就是测量文本宽度。由于需要测量文本在哪个字符处换行,如果测量不准,那就会有明显的闪动。我调研了一下服务端测量文本的几种方案,发现没有任何一种方案可以直接准确的测量。
  40. 那就代表没有办法进行文本测量了吗?所以我先去看了一下谷歌的方案。谷歌采用了提前测量宽度的方式,我们知道文字一般分为全角和半角,对于全角来说一般都是等宽的,可以提前测好任意一个字符宽度。半角可能等宽也可能不等宽,emoji一般也是等宽的。所以他们对abc123这些半角字符提前测量保存在本地,然后逐字测量,累加到一起,要是没法命中,那就当做等宽字符处理。所以这里问题比较大,我们无法把所有不等宽的字符(比如泰文、越南文等)都提前测好写死在本地,所以当做等宽字体来处理就非常不准确。
  41. 其次,将文本长度逐个累加也不准确,因为文本排版中会有kerning 存在,可以看到VA和MN中间的间距是不一样的,如果MN中间间距很小,那就会连起来。所以不能单纯累加单个字符宽度。
  42. 基于上面的一些经验,我这里最终对文本进行了分词,将会产生kerning的单词放到一起。然后把拆分后的单词传给 skia-canvas 测量,将测量过的单词宽度进行缓存,避免重复测量。对于 emoji 这种无法准确测量的字符,默认当做等宽字体来处理,这样还原准确度就远远大于竞品了。
  43. 可以看一下我们 ssr 最后的还原效果,整体是还原度是比较高的,差异很小。
  44. 后续我们打算在流水线中增加对主流浏览器服务端渲染和实际绘制差异的分析对比。然后利用服务度渲染函数支持smartsheet的打印功能,最后还要实现一套虚拟 dom,用来替换依赖的 jsdom 运行环境。
  45. 最后一部分就是关于我的技术影响力和输出。
  46. 我去年负责了武汉大学前端菁英班这件事,教授一个学分16课时的选修课程,有50名计算机学院学生参与。我负责了课程设计、线下授课、辅导、考核、实习生选拔全流程,招募武汉大学实习生7人。
  47. 我是慕课网1300本销量《web前端开发修炼指南》的作者。我也是掘金优秀作者,公众号《前端小馆》的作者,去年输出文章20篇。我有多篇文章上了 km 推荐和头条。
  48. 这里几个附录一个是关于安全方面的,另一个就是提高开发效率方面的。
  49. 最后就是渲染层设计和首片数据的数据。我的演讲到这里就结束了,谢谢大家。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注