在实际开发中,很多人会认为“千万级数据表格”属于不切实际的前端场景,尤其是浏览器端。但在实际业务中,类似飞书文档、Excel Online、乃至企业级 ERP 系统,确实存在这样的需求。
以飞书为例,一个普通在线表格的初始化是 200 行 × 20 列,用户可以随时无限扩展行列,甚至进行协同编辑、插入图文音视频、公式计算、AI 生成等功能。这些都对表格渲染系统提出了极高的性能与架构要求。
1.0:传统 DOM 表格实现(性能瓶颈的起点)
在早期阶段,我们往往直接使用 <table>
元素,或是使用如 Ant Design Table、Element Plus Table 等组件库,它们底层都依赖原生 DOM 表格结构。代码内容在baseTable.vue
问题很快出现:
-
当数据量增长至几千行几百列时,性能急剧下滑:
- 页面卡顿、渲染延迟甚至白屏
- DOM 节点数爆炸式增长,占用内存飙升
- 滚动/交互操作响应缓慢
为什么会这样?
因为传统 DOM 表格是“全量渲染”,即便屏幕上只能显示几十行数据,也会将所有数据一股脑渲染到页面,极度浪费资源。
2.0:虚拟滚动表格(Virtual Scrolling)
为了解决上述问题,我们引入了“虚拟渲染”思路:只渲染可视区域内的数据。代码内容在vistualTable.vue
核心思路:
- 将
<table>
改为<div>
结构 - 自行实现类似表格的样式和网格排布
- 根据滚动位置计算当前“可见行/列”的索引范围,仅渲染这部分数据
- 引入
overscan
(预渲染缓冲区),避免滚动撕裂感
优点:
- 极大减少 DOM 数量,显著降低内存占用和重排开销
- 用户体验更流畅,滚动性能提升显著
局限:
- 当渲染单元格本身逻辑较重(如嵌套组件、图表)时,依然可能卡顿
- DOM 本身仍有一定上限(比如 1w+ 节点依然可能不稳定)
3.0:Canvas 绘制表格(突破 DOM 瓶颈)
DOM 的结构性和灵活性是优势,但也是性能瓶颈所在。为了进一步提升性能,我们尝试用 <canvas>
实现表格渲染。代码内容在canvasTable.vue
原理:
- 抛弃传统 DOM,改用 canvas 直接在像素级别绘制单元格、文本
- 所有单元格渲染逻辑都基于
canvasContext.drawXXX()
API
优势:
- 极致性能:100 万行数据可在 800ms 内渲染完成(相比 DOM 的 12s)
- 更低内存占用:只需保留数据缓存和一个绘图缓冲区(几十 MB 级别)
- 不存在 DOM 重排和合成层切换问题
挑战:
- 内容溢出判断缺失:Canvas 不具备 DOM 那样的自动布局与样式计算能力,无法像
<div>
那样自动检测内容是否超出边界,因此需要开发者手动计算每个单元格文本的宽度与高度(可使用measureText
),并控制截断、省略或自动换行。 - 缺失原生滚动行为:Canvas 不自带滚动条,不能像 DOM 元素那样通过 overflow 滚动,因此通常需要开发者自定义实现“虚拟滚动条”——包括轨道绘制、拖拽处理、同步 scrollTop/scrollLeft 等。
- 状态同步问题:由于 Canvas 是“无状态”的像素画布,不具备 DOM 那样的实时属性反映能力,因此选中状态、hover 效果、输入聚焦等交互状态必须在数据模型中单独维护,并在绘制时显式处理。
- 组件层叠问题:当需要在 Canvas 上叠加组件(如输入框、下拉框)时,必须计算其准确位置,并确保在 scroll 或缩放操作时实时更新位置,避免“组件飘移”或“错位”。
- 缩放支持复杂:相比 DOM 的 transform: scale,Canvas 缩放涉及多个层面:坐标变换、像素倍率(devicePixelRatio)、字体缩放等,需精细控制渲染比例与锚点,避免出现模糊或绘制错位。
- 多画布分层策略:为提升性能,实际开发中通常会将表格拆为多个 canvas 层,如:背景层、内容层、选中高亮层、组件层,避免全量重绘,提高渲染粒度控制力。
- 文本抗锯齿差:
fillText
渲染清晰度低,可通过 SVGforeignObject
混合模式规避 - 缺乏事件绑定机制:需手动进行命中检测(点击事件 → 坐标映射 → 单元格索引)
- 可访问性缺失:需要维护虚拟 DOM 以供屏幕阅读器识别
4.0:Canvas + Tile 分块渲染(性能与体验平衡)
随着数据进一步增长,纯 canvas 的“整屏渲染”仍然面临帧率波动、GPU 压力等问题。我们进一步引入 可视化 Tile 区块渲染机制。代码内容在canvasVirtualTable.vue
技术方案:
- 将表格逻辑划分为多个“瓷砖(tile)”区域
- 仅渲染视口区域及其周边缓冲 tile
- 实现局部更新、惰性加载,提升性能与响应速度
这种 tile-based 渲染模型,已经被飞书、Notion 等广泛采用,是兼顾性能与扩展能力的重要设计。
5.0:WASM 高性能数据处理引擎
尽管渲染优化已较为成熟,数据处理本身(如排序、搜索、筛选、合并)依然是性能瓶颈。尤其在需要生成、处理千万条数据的场景中,纯 JS 已显力不从心。代码内容在webAssemblyTable.vue/canvasWebAssemblyTable.vue,具体构建详情可见下文
WebAssembly(WASM)的优势:
- 可将核心逻辑用 C++/Rust 编写,编译为 Wasm 模块
- 运算性能比 JS 快 3~5 倍
- 可利用共享内存(SharedArrayBuffer)实现零拷贝数据传递
- 不受 JS 引擎垃圾回收机制影响,内存管理更高效
为什么不是 Web Worker?
虽然 Worker 可将计算任务移出主线程,但:
- 依旧运行在 JS 引擎之上,速度提升有限
- 数据传输需要序列化(postMessage),处理亿级数据时极其缓慢
最佳实践:WASM + Worker 混合架构
Web Worker:负责调度任务,管理 UI 通信
WASM 模块:在 Worker 中加载运行,处理核心算法逻辑
SharedArrayBuffer:用于 UI 与计算层之间共享数据,零拷贝通信
技术总览图
技术阶段 | 渲染模式 | 性能瓶颈 | 适用场景 |
---|---|---|---|
DOM Table | 全量渲染 | DOM 数量爆炸 | 小规模数据展示 |
虚拟滚动 | 可视区域渲染 | 单元格复杂时仍卡顿 | 万级数据轻交互场景 |
Canvas | 像素绘制 | 交互开发复杂 | 百万级只读/轻交互表格 |
Tile Canvas | 分块绘制 | 动态组件叠加较复杂 | 百万级高频交互表格 |
Wasm+Worker | 模块化计算 | 架构集成成本较高 | 亿级数据处理、边渲染边生成场景 |
总结
千万级数据表格渲染早已不是“不可能完成的任务”。但它要求开发者在架构、性能优化、用户体验、甚至底层图形和内存管理等多方面不断提升。
从传统 DOM 到 Canvas + WASM 混合架构,这背后反映的是前端从页面渲染走向真正的“系统开发”的演进过程。