Skip to content
行 + 列双向虚拟化TypeScript零 UI 框架依赖14 个可选插件

VirtTable十万行数据,一屏 DOM

一套框架无关的虚拟表格内核。Vanilla / Vue / React 共用同一份逻辑,复杂表格能力齐备, 每条性能结论都有实测数字撑着。这一页里跑着的就是真的 10 万行,不是截图。

$npm i @virt-table/vanilla
100,000 行 × 14 列 · 140 万单元格
正在装载 10 万行…
实时帧率
fps
常驻行 DOM
≤—tr
视口首行
#0
数据总量
100,000

左右滚动看列虚拟化,点「跳到第 10 万行」看纵向。HUD 读数现场采样,行 DOM 取实测上界。这张表不开合并 —— 合并单独放在下一屏演。

Merged cells, still virtual

合并单元格之下,行与列照样只渲染一屏

合并是虚拟表格最容易破功的地方:一个 rowspan 跨两万行的格子, 天真的实现只能把它物化成两万个 <tr>,虚拟滚动当场失效。 下面这张表 10 万行、三档合并段同屏、每 20 行还横插一条跨列汇总带 —— HUD 里那条「真实跨度 → DOM rowspan」就是全部答案。

100,000 行 × 25 列 · 合并全开
进入视口后装载 10 万行…
合并段真实跨度 → DOM rowspan
不在 DOM 里
单元格 DOM / 总量
/ 2,500,000
常驻行 DOM
≤—tr
渲染中的合并块 · 帧率
·fps

视口首行 #1 · 当前行中间段 colspan 合计 —/— —— 这条恒等式在任何横滚位置都成立,跨界的合并格一个都没丢,所以行永远对得上表头。 「业务域」段长 2 万行、「区域」2 千行、「班组」8 行三档同屏;每 20 行一条汇总带横跨「本次运行」整组 4 列。合并列自己横滚出窗口时第一格会显示「—」:那几列此刻确实不在 DOM 里。

长段不展开,只存段起点

按值自动合并把段起点存进 Int32Array,查询走二分、每帧按渲染窗口裁剪。所以「业务域」那一段真实跨两万行,DOM 里的 rowspan 却只有十几 —— 与数据量无关。

重锚是必然,不是妥协

滚进长段中间时,合并格的起点被抬到窗口起点,内容锚在可视区(AG Grid 的 Cell Span 同样如此)。换来的是渲染区间**不被拉宽** —— 这正是让自动合并在虚拟滚动下成立的关键。标签的逐帧跟随交给 position: sticky,主线程零开销。

跨列合并走另一条路

按值合并只产出纵向段;任意几何用静态 merges。横滚后合并带的起始列会落到窗口左侧之外,此时渲染列区间被反向撑回去,屏外那段折叠成**一个**留白格 —— 行的 colspan 始终配平。

Measured

性能不是形容词,是数字

下面每一项都能复现:纯逻辑走 vitest,DOM 行为与帧耗时走 Chrome DevTools Protocol(headless + 合成器手势滚动)。测法与陷阱都写在文档里。

8.3ms
10 万行滚动帧耗时中位

p95 9.2ms;开按值自动合并后与不开完全一致

25tr
常驻行 DOM

与数据量无关;滚到 9.3 万行长段中间仍是 25

0%
表头空白帧

把 thead/tfoot 移出 body table 之前是 41%

3.3ms
100 万行建合并段索引

Int32Array 存段起点,占 781 KB,零对象分配

27.1kB
基础表格需引入的 CSS

插件与内置组件各自成文件,全量 45 kB

1040
单元测试

36 个测试文件,含 4 份契约测试与 17 份 DOM 测试

测量条件、复现步骤与已被证伪的假设,见 设计决策架构设计

How it stays smooth

流畅度是拆出来的,不是调参调出来的

虚拟滚动最难的部分不是「算哪些行可见」,而是每帧只做必要的那点工作、并且不让 浏览器替你重算全表。这六条都是踩过之后定下来的。

01

行 + 列双向虚拟化

纵向窗口由框架无关的 @virt-list/core 算,横向由列前缀和 + colBuffer 自己裁。宽表和长表都只把「看得见的那一屏」变成 DOM。

常驻 25 个 tr10 万行 × 30 列与 1000 行同价
02

表头 / 表尾独立成层

<table> 是整体布局上下文:虚拟滚动每帧改 tbody,会让内容根本没变的 <thead> 一起失效、重新光栅化,主线程忙时就是整条表头闪白。于是把它们搬成滚动容器内的独立 sticky table。

41% → 0%表头空白帧,唯一变量就是「在不在 body table 里」
03

表体写 top 残差,不用 transform

transform 会给后代建立包含块,让固定列的 position: sticky 失去参照、跟着内容被推走。写残差不建包含块,还顺手消掉了元素高度上限与「滚不到底」两个老坑。

0.03px横滚 30 帧固定表头最大偏差,纯亚像素舍入
04

合并段只存段起点,不展开

按值自动合并把段起点存进 Int32Array,查询走二分、每帧按渲染窗口裁剪。展开成全量 merges 会把 rowspan=50000 物化成 5 万个 <tr>,虚拟滚动直接失效。

快 2–8 倍10 万次查询:二分 0.4–2.3ms,Map 3–9.7ms
05

滚动路径上不读布局

tooltip 的文本截断判定放进延迟计时器,而不是 mouseover 里。滚动时指针底下的行在动,布局是脏的,那一刻读 scrollWidth 会强制同步布局。

145 倍读 scrollWidth:布局干净 0.15µs,布局脏 21.8µs
06

滑块按行索引映射,不按像素

不定高表格的内容总高随实测行高一直回填,按像素算滑块位置会让它在手指底下反复跳。所以两条滚动条都自绘成 overlay 浮层,分母取「滚到底时视口顶部那一行」。

连续跟手frac 让高行内部也能连续移动,不吸附到行边界

One core, three bindings

三端一套逻辑,不是三份实现

所有表格行为都在框架无关的 vanilla 层。Vue 与 React 只做 VNode / JSX ↔ DOM 的桥接和生命周期,因此 API、事件与插件在三端逐条对齐;插件的方法还会自动透传到 组件 ref 上。

示例也按端各出一份:Vanilla 76 个,Vue / React 同步跟进。

import { VirtTable } from '@virt-table/vanilla';
import '@virt-table/vanilla/core.css';

const table = new VirtTable(el, {
  list,              // 10 万行照常整个传进来
  columns,
  itemKey: 'id',
  estimatedSize: 40, // 行高估值,动态行高会自动回填
});

table.scrollToIndex(99_999);

→ Vanilla 快速开始

Capabilities

复杂表格该有的,都在

核心只留「不可选」的部分,其余做成插件与内置组件按需装载 —— 只用基础表格时 CSS 27 kB,全量装齐 45 kB。

表格结构 7

  • 行 + 列双向虚拟化,动态行高自动测量
  • 冻结列(左 / 右)与固定行 pinned
  • 多级分组表头 / 表尾,分组标签跟随横滚
  • 合并单元格:表体 / 表头 / 表尾
  • 按值自动合并 mergeKey,且不禁用排序
  • 树形 · 分组 · 树形 + 分组 · 展开行
  • 自动换行 / 溢出省略 / 悬停 tooltip

类电子表格交互 8

  • 单元格区域框选,含边缘自动滚动
  • 键盘导航:方向键 / PageUp·Down / Ctrl+Home·End
  • 剪贴板复制粘贴,与 Excel 互通
  • 单元格编辑浮层 + 校验
  • 列宽拖拽,指针事件一套代码覆盖触屏
  • 列拖拽排序 / 行拖拽排序
  • 右键菜单、列显隐面板
  • 实验中的电子表格模式

数据与流程 8

  • 统一管线:筛选 → 排序 → 分页 → 结构展开 → 聚合
  • 客户端分页(默认)与服务端分页
  • 无限滚动(向下 / 向上 / 双向)
  • 树形懒加载、服务端取数状态机
  • 列头筛选 + 高级筛选管理器
  • 自动合计行、全表搜索
  • CSV / Excel 导出与打印
  • 状态持久化 getState / setState

AI 与工程化 9

  • vtAIQuery 自然语言查询,不绑任何模型厂商
  • vtAIColumn AI 列,只为可见行付费
  • vtAIStream 流式表格,列从首批数据推断
  • 14 个插件按需装载,tree-shaking 友好
  • 17 个内置单元格组件
  • 第三方组件库列渲染(Element Plus / Ant Design)
  • i18n:内置 zhCN / enUS,可运行时切换
  • 无障碍 ARIA 网格语义
  • 亮色 / 暗色主题,--vt-* 令牌换肤

Engineering

能长期维护,才敢叫专业

表格是最容易在第二年崩掉的组件:行高、列宽、滚动几何、样式特异度互相牵扯, 改一处坏三处。所以这里的护栏不止单元测试。

1040

单元测试,36 个文件

纯逻辑跑 node 环境,DOM 行为跑 happy-dom 并按文件开启 —— 避免 11 个纯逻辑文件白付每文件约 90ms 的环境成本。

4

份契约测试

CSS 令牌算术(行高等式)、JS 写出的 class ↔ CSS 选择器、内置组件契约、文档站样式与 demo 的隔离。守的是编译器管不到、运行时也测不出来的耦合。

CDP

性能结论全部实测

Chrome headless 里用合成器手势驱动真滚动(synthesizeScrollGesture),按区带亮度判空白帧。复现步骤写在文档里,包括两个会把人带偏的测量陷阱。

20+

条决策记录,含否决理由

列宽内容自适应、逐格 spanMethod、tooltip 挂 body —— 每条为什么不做都写清了依据,避免下一个人再走一遍。

React / Vue 适配层VNode / JSX ↔ DOM 桥接、生命周期、列转换薄封装
插件层框选 / 键盘 / 编辑 / 筛选 / 拖拽 / 搜索 / 导出 / AI14 个,可选
VirtTable 核心(vanilla)列管理 · 固定列 · 横向虚拟化 · 合并 · 树形分组 · 多级表头 · 单元格几何框架无关
@virt-list/core纵向虚拟滚动引擎,纯算法不碰 DOM外部依赖

十万行,先滚起来看看

装一个包、传一个 listcolumns,剩下的按需加。