每一个前端都想作一个完美的表格,业界也在持续探索不一样的思路,好比钉钉表格、语雀表格。前端
笔者所在数据中台团队也对表格有着极高的要求,尤为是自助分析表格,须要兼顾性能与交互功能,本文即是记录自助分析表格高性能的研发思路。git
要作表格首先要选择基于 DOM 仍是 Canvas,这是技术选型的第一步。好比钉钉表格就是 基于 Canvas 实现的,固然这不表明 Canvas 实现就比 DOM 实现要好,从技术上各有利弊:github
技术选型要看具体的业务场景,钉钉表格其实就是在线 Excel,Excel 这种形态决定了单元格内必定是简单文本加一些简单图标,所以不用考虑渲染自定义内容的场景,因此选择 Canvas 渲染在将来也不会遇到很差拓展的麻烦。web
而自助分析表格自然可能拓展图形、图片、操做按钮到单元格中,对轴的拖拽响应交互也很是复杂,为了避免让 Canvas 成为之后拓展的瓶颈,仍是选择 DOM 实现比较稳当。数组
那问题来了,既然 DOM 渲染效率自然比 Canvas 低,咱们应该如何用 DOM 实现一个高性能表格呢?浏览器
其实业界已经有许多 DOM 表格优化方案了,主要以按需渲染、虚拟滚动为主,即预留一些 Buffer 区域用于滑动时填充,表格仅渲染可视区域与 Buffer 区域部分。但这些方案都不可避免的存在快速滑动时白屏问题,笔者经过不断尝试终于发现了一种完美解决的方案,咱们一块儿往下看吧!微信
即每一个单元格都是用绝对定位的 DIV 实现,整个表格都是有独立计算位置的 DIV 拼接而成的:框架
这样作的前提是:性能
带来的好处是:优化
如图所示有 16 个单元格,当咱们向右下滑动一格时,中间 3x3 即 9 个格子的区域是彻底不会从新渲染的,这样零散的绝对定位分布能够最大程度维持单元格原本的位置。咱们能够认为,任何一格单元格只要自身不超出屏幕范围,就不会随着滚动而重渲染。
若是你采用 React 框架来实现,只要将每一个格子的 key 设置为惟一的便可,好比当前行列号。
通常来讲,轴由于逻辑特殊,其渲染逻辑和单元格会分开维护,所以咱们将表格分为三个区域:横轴、纵轴、单元格。
显然,常识是横轴只能纵向滚动,纵轴只能横向滚动,单元格能够横纵向滚动,那么横向和纵向滚动条就只能出如今单元格区域:
这样会存在三个问题:
.scroll
模拟滚动,这必然会致使单元格与轴滚动有必定错位,即轴的滚动有几毫秒的滞后感。overflow: auto
的,而轴区域 overflow: hidden
没法触发滚动。通过一番思考,咱们只要将方案稍做调整,就能同时解决上面三个问题:即不要使用原生的滚动条,而是使用 .scroll
代替滚动,用 mousewheel
监听滚动的触发:
这样作带来什么变化呢?
.scroll
触发滚动,使得轴和单元格不会出现错位,由于轴和单元格都是用 .scroll
触发的滚动。overflow
属性。js
控制触发的滚动发生在渲染完成以后,因此浏览器会在滚动发生前现完成渲染,这至关有趣。模拟滚动时,实际上整个表格都是 overflow: hidden
的,浏览器就不会给出自带滚动条了,咱们须要用 DIV 作出虚拟滚动条代替,这个相对容易。
当咱们采用模拟滚动方案时,至关于采用了在滚动时 “高频渲染” 的方案,所以不须要使用截留,更不要使用 Buffer 区域,由于更大的 Buffer 区域意味着更大的渲染开销。
当咱们把 Buffer 区域移除时,发现整个屏幕内渲染单元格在 1000 个之内时,现代浏览器甚至配合 Windows 都能快速完成滚动前刷新,并不会影响滚动的流畅性。
固然,滚动过快依然不是一件好事,既然滚动是由咱们控制的,能够稍许控制下滚动速度,控制在每次触发 mousewheel
位移不超过 200 左右最佳。
像单元格合并、行列隐藏、单元格格式化等计算逻辑,最好在滚动前提早算掉,不然在快速滚动时实时计算必然会带来额外的计算成本损耗。
可是这种预计算也有弊端,当单元格数量超过 10w 时,计算耗时通常会超过 1 秒,单元格数量超过 100w 时,计算耗时通常会超过 10 秒,用预计算的牺牲换来滚动的流畅,仍是有些遗憾,咱们能够再思考如下,可否下降预计算的损耗?
局部预计算就是一种解决方案,即使单元格数量有一千万个,但咱们若是仅计算前 1w 个单元格呢?那不管数据量有多大,都不会出现丝毫卡顿。
但局部预计算有着明显缺点,即表格渲染过程当中,局部计算结果并不总等价于全局计算结果,典型的有列宽、行高、跨行跨列的计算字段。
咱们须要针对性解决,对于单元格宽高计算,必须采用局部计算,由于全量计算的损耗很是大。但局部计算确定是不许确的,以下图所示:
但出于性能考虑,咱们初始化可能仅能计算前三行的高度,此时,咱们须要在滚动时作两件事情:
这样滚动过程当中虽然单元格会被忽然撑开,但位置并不会产生相对移动,与提早全量撑开后视觉内容相同,所以用户体验并不会有实际影响,但计算时间却由 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 了,不可能出现这种规模的聚合数据。
前端计算还能够采用多个 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 许可证)