精读《高性能表格》

每一个前端都想作一个完美的表格,业界也在持续探索不一样的思路,好比钉钉表格、语雀表格。前端

笔者所在数据中台团队也对表格有着极高的要求,尤为是自助分析表格,须要兼顾性能与交互功能,本文即是记录自助分析表格高性能的研发思路。git

精读

要作表格首先要选择基于 DOM 仍是 Canvas,这是技术选型的第一步。好比钉钉表格就是 基于 Canvas 实现的,固然这不表明 Canvas 实现就比 DOM 实现要好,从技术上各有利弊:github

  • Canvas 渲染效率比 DOM 高,这是浏览器实现致使的。
  • DOM 可拓展性比 Canvas 好,渲染自定义内容首选 DOM 而非 Canvas。

技术选型要看具体的业务场景,钉钉表格其实就是在线 Excel,Excel 这种形态决定了单元格内必定是简单文本加一些简单图标,所以不用考虑渲染自定义内容的场景,因此选择 Canvas 渲染在将来也不会遇到很差拓展的麻烦。web

而自助分析表格自然可能拓展图形、图片、操做按钮到单元格中,对轴的拖拽响应交互也很是复杂,为了避免让 Canvas 成为之后拓展的瓶颈,仍是选择 DOM 实现比较稳当。数组

那问题来了,既然 DOM 渲染效率自然比 Canvas 低,咱们应该如何用 DOM 实现一个高性能表格呢?浏览器

其实业界已经有许多 DOM 表格优化方案了,主要以按需渲染、虚拟滚动为主,即预留一些 Buffer 区域用于滑动时填充,表格仅渲染可视区域与 Buffer 区域部分。但这些方案都不可避免的存在快速滑动时白屏问题,笔者经过不断尝试终于发现了一种完美解决的方案,咱们一块儿往下看吧!微信

单元格使用 DIV 绝对定位

即每一个单元格都是用绝对定位的 DIV 实现,整个表格都是有独立计算位置的 DIV 拼接而成的:框架

这样作的前提是:性能

  1. 全部单元格位置都要提早计算,这里能够利用 web worker 作并行计算。
  2. 单元格合并仅是产生一个更大的单元格,它的定位方式与小单元格并没有差别。

带来的好处是:优化

  1. 滚动时,单元格能够最大程度实现复用。
  2. 对于合并的单元格,只会让可视区域渲染的总单元格数更小,更利于性能提高,而不是带来性能负担。

如图所示有 16 个单元格,当咱们向右下滑动一格时,中间 3x3 即 9 个格子的区域是彻底不会从新渲染的,这样零散的绝对定位分布能够最大程度维持单元格原本的位置。咱们能够认为,任何一格单元格只要自身不超出屏幕范围,就不会随着滚动而重渲染。

若是你采用 React 框架来实现,只要将每一个格子的 key 设置为惟一的便可,好比当前行列号。

模拟滚动而非原生滚动

通常来讲,轴由于逻辑特殊,其渲染逻辑和单元格会分开维护,所以咱们将表格分为三个区域:横轴、纵轴、单元格。

显然,常识是横轴只能纵向滚动,纵轴只能横向滚动,单元格能够横纵向滚动,那么横向和纵向滚动条就只能出如今单元格区域:

这样会存在三个问题:

  1. 单元格使用原生滚动,横纵轴只能在单元格区域监听滚动后,经过 .scroll 模拟滚动,这必然会致使单元格与轴滚动有必定错位,即轴的滚动有几毫秒的滞后感。
  2. 鼠标放在轴上时没法滚动,由于只有单元格是 overflow: auto 的,而轴区域 overflow: hidden 没法触发滚动。
  3. 快速滚动出现白屏,即使留了 Buffer 区域,在快速滚动时也无能为力,这是由于渲染速度跟不上滚动致使的。

通过一番思考,咱们只要将方案稍做调整,就能同时解决上面三个问题:即不要使用原生的滚动条,而是使用 .scroll 代替滚动,用 mousewheel 监听滚动的触发:

这样作带来什么变化呢?

  1. 轴、单元格区域都使用 .scroll 触发滚动,使得轴和单元格不会出现错位,由于轴和单元格都是用 .scroll 触发的滚动。
  2. 任何位置都能监听滚动,使得轴上也能滚动了,咱们再也不依赖 overflow 属性。
  3. 快速滚动时惊喜的发现不会白屏了,缘由是用 js 控制触发的滚动发生在渲染完成以后,因此浏览器会在滚动发生前现完成渲染,这至关有趣。

模拟滚动时,实际上整个表格都是 overflow: hidden 的,浏览器就不会给出自带滚动条了,咱们须要用 DIV 作出虚拟滚动条代替,这个相对容易。

零 buffer 区域

当咱们采用模拟滚动方案时,至关于采用了在滚动时 “高频渲染” 的方案,所以不须要使用截留,更不要使用 Buffer 区域,由于更大的 Buffer 区域意味着更大的渲染开销。

当咱们把 Buffer 区域移除时,发现整个屏幕内渲染单元格在 1000 个之内时,现代浏览器甚至配合 Windows 都能快速完成滚动前刷新,并不会影响滚动的流畅性。

固然,滚动过快依然不是一件好事,既然滚动是由咱们控制的,能够稍许控制下滚动速度,控制在每次触发 mousewheel 位移不超过 200 左右最佳。

预计算

像单元格合并、行列隐藏、单元格格式化等计算逻辑,最好在滚动前提早算掉,不然在快速滚动时实时计算必然会带来额外的计算成本损耗。

可是这种预计算也有弊端,当单元格数量超过 10w 时,计算耗时通常会超过 1 秒,单元格数量超过 100w 时,计算耗时通常会超过 10 秒,用预计算的牺牲换来滚动的流畅,仍是有些遗憾,咱们能够再思考如下,可否下降预计算的损耗?

局部预计算

局部预计算就是一种解决方案,即使单元格数量有一千万个,但咱们若是仅计算前 1w 个单元格呢?那不管数据量有多大,都不会出现丝毫卡顿。

但局部预计算有着明显缺点,即表格渲染过程当中,局部计算结果并不总等价于全局计算结果,典型的有列宽、行高、跨行跨列的计算字段。

咱们须要针对性解决,对于单元格宽高计算,必须采用局部计算,由于全量计算的损耗很是大。但局部计算确定是不许确的,以下图所示:

但出于性能考虑,咱们初始化可能仅能计算前三行的高度,此时,咱们须要在滚动时作两件事情:

  1. 在快速滚动的时候,向 web worker 发送预计要滚动到的位置,增量计算这些位置文字宽度,并实时修正列总宽。(由于列总宽算完只要存储最大值,因此已计算的数量级会被压缩为 O(1))。
  2. 宽度计算完毕后,快速刷新当前屏幕单元格宽度,但在宽度校准的同时,维持可视区域内左对齐不变,以下图所示:

这样滚动过程当中虽然单元格会被忽然撑开,但位置并不会产生相对移动,与提早全量撑开后视觉内容相同,所以用户体验并不会有实际影响,但计算时间却由 O(row * column) 降低到 O(1),只要计算一个常数量级的单元格数目。

计算字段也是同理,能够在滚动时按片预计算,但要注意仅能在计算涉及局部单元格的状况下进行,若是这个计算是全局性质的,好比排名,那么局部排序的排名确定是错误的,咱们必须进行全量计算。

好在,即使是全量计算,咱们也只须要考虑一部分数据,假设行列数量都是 n,能够将计算复杂度由 O(n²) 下降为 O(n):

这种计算字段的处理没法保证支持无限数量级的数据,但能够大大下降计算时间,假设 1000w 单元格计算时间开销是 60s,这是一个几乎不能忍受的时间,假设 1000w 单元格是 1w 行 * 1k 列造成的,咱们局部计算的开销是 1w 行(100ms) + 1k 列(10ms) = 0.1s,对用户来讲几乎感觉不到 1000w 单元格的卡顿。

在 10w 行 * 10w 列的状况下,等待时间是 1+1 = 2s,用户会感觉到明显卡顿,但总单元格数量但是惊人的 100 亿,光数据可能就几 TB 了,不可能出现这种规模的聚合数据。

Map Reduce

前端计算还能够采用多个 web worker 加速,总之不要让用户电脑的 CPU 闲置。咱们能够经过 window.navigator.hardwareConcurrency 获取硬件并行能支持的最大 web worker 数量,咱们就实例化等量的 web worker 并行计算。

拿刚才排名的例子来讲,一样 1000w 单元格数量,若是只有一列呢?那行数就是扎扎实实的 1000w,这种状况下,即使 O(n) 复杂度计算耗时也可能突破 60s,此时咱们就能够分段计算。个人电脑 hardwareConcurrency 值为 8,那么就实例化 8 个 web worker,分别并行计算第 0 ~ 125w, 125w ~ 250w ..., 875w ~ 1000w 段的数据分别进行排序,最后获得 8 段有序序列,在主 worker 线程中进行合并。

咱们能够采用分治合并,即针对依次收到的排序结果 x1, x2, x3, x4...,将收到的结果两两合并成 x12, x34, ...,再次合并为 x1234 直到合并为一个数组为止。

固然,Map Reduce 并不能解决全部问题,假设 1000w 数据计算耗时 60s,咱们分为 8 段并行,每一段平均耗时 7.5s,那么第一轮排序总耗时为 7.5s。分治合并时间复杂度为 O(kn logk),其中 k 是分段数,这里是 8 段,logk 约等于 3,每段长度 125w 是 n,那么一个 125w 数量级的二分排序耗时大概是 4.5s,时间复杂度是 O(n logn),因此等价为 logn = 4.5s, k x logk 等于几?这里因为 k 远小于 n,因此时间消耗会远小于 4.5s,加起来耗时不会超过 10s。

总结

若是你想打造高性能表格,DIV 性能足够了,只要注意实现的时候稍加技巧便可。你能够用 DIV 实现一个兼顾性能、拓展性的表格,是时候从新相信 DOM 了!

笔者建议读完本文的你,按照这样的思路作一个小 Demo,同时思考,这样的表格有哪些通用功能能够抽象?如何设计 API 才能成为各种业务表格的基座?如何设计功能才能知足业务层表格繁多的拓展诉求?

讨论地址是: 精读《高性能表格》· Issue #309 · dt-fe/weekly

若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证
相关文章
相关标签/搜索