16 KiB
虚拟滚动优化
**本文引用的文件** - [FmsTable.vue](file://fms-vue/src/components/fms-table/FmsTable.vue) - [useVirtualScroll.ts](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts) - [const.ts](file://fms-vue/node_modules/stk-table-vue/src/StkTable/const.ts) - [package.json](file://fms-vue/package.json) - [fms-table-virtual.spec.js](file://fms-vue/tests/fms-table-virtual.spec.js) - [measure-wheel.mjs](file://.fps-probe/measure-wheel.mjs)目录
简介
本技术文档聚焦 FMS 表格系统的虚拟滚动能力,围绕 StkTable 库的虚拟滚动实现进行深度解析。内容涵盖可视区域计算、DOM 节点复用与内存管理、触发条件(MIN_VIRTUAL_ROW_COUNT 阈值)、性能优化策略(requestAnimationFrame 合并重排、ResizeObserver 防抖处理)、X 轴与 Y 轴虚拟滚动的差异,以及 colResizable 列宽调整时的性能考虑。文末提供不同数据量场景下的配置建议与最佳实践,帮助在大数据量渲染中取得稳定流畅的体验。
项目结构
FMS 表格由上层 FmsTable 组件封装 stk-table-vue 提供的 StkTable 组件,并通过自定义逻辑控制虚拟滚动开关、尺寸变化刷新与编辑态渲染路径。测试用例验证了在不同数据规模下 DOM 节点数量保持在可视区范围内。
graph TB
A["FmsTable.vue<br/>封装与业务逻辑"] --> B["StkTable.vue<br/>stk-table-vue 表格组件"]
B --> C["useVirtualScroll.ts<br/>Y/X 虚拟滚动核心"]
B --> D["const.ts<br/>默认常量/行高/尺寸"]
E["fms-table-virtual.spec.js<br/>虚拟滚动行为断言"] --> A
F["measure-wheel.mjs<br/>滚动帧率与渲染行数测量"] --> A
图表来源
- FmsTable.vue:1-800
- useVirtualScroll.ts:1-814
- const.ts:1-51
- fms-table-virtual.spec.js:1-154
- measure-wheel.mjs:63-96
章节来源
核心组件
- FmsTable 组件:负责虚拟滚动开关决策、尺寸变化监听与刷新、编辑态渲染路径优化、样式增强(合成层提升)。
- StkTable + useVirtualScroll:提供 Y 轴与 X 轴虚拟滚动核心算法,包括可视范围计算、列合并修正、固定列处理、自动行高缓存等。
- 测试与测量:通过单元测试验证 DOM 渲染规模;通过 FPS 探针评估滚动帧率与渲染行数。
章节来源
- FmsTable.vue:10-76
- useVirtualScroll.ts:99-175
- fms-table-virtual.spec.js:30-77
- measure-wheel.mjs:63-96
架构总览
FmsTable 将虚拟滚动开关 effectiveVirtual 基于 MIN_VIRTUAL_ROW_COUNT 阈值动态计算,避免小数据量时因虚拟化导致的“吞掉最后一行”问题。StkTable 内部通过 useVirtualScroll 维护 Y 轴与 X 轴的可视范围、偏移量与分页大小,并在滚动事件中更新 startIndex/endIndex,仅渲染可见行列。
sequenceDiagram
participant App as "应用"
participant FT as "FmsTable.vue"
participant ST as "StkTable.vue"
participant VS as "useVirtualScroll.ts"
participant DOM as "浏览器DOM"
App->>FT : 传入 dataSource / columns / props
FT->>FT : 计算 effectiveVirtual(>MIN_VIRTUAL_ROW_COUNT)
FT->>ST : 绑定 virtual/virtualX/auto-resize/col-resizable
ST->>VS : initVirtualScroll()
VS->>VS : 计算 pageSize/offsetTop/translateY
DOM-->>VS : scroll/resize 事件
VS->>VS : updateVirtualScrollY/X()
VS-->>ST : 返回 startIndex/endIndex/offsetLeft/offsetTop
ST-->>DOM : 仅渲染可视区 tr/td
图表来源
- FmsTable.vue:41-68
- useVirtualScroll.ts:479-521
- useVirtualScroll.ts:561-712
- useVirtualScroll.ts:719-789
详细组件分析
FmsTable 虚拟滚动开关与刷新机制
- 阈值控制:当数据行数超过 MIN_VIRTUAL_ROW_COUNT(100)时才启用虚拟滚动,避免小数据量下虚拟化带来的“最后几行不可见”问题。
- 尺寸变化处理:使用 ResizeObserver 监听容器尺寸变化,采用 trailing 防抖(约 150ms),仅在尺寸稳定后调用 initVirtualScroll/initVirtualScrollX,减少动画期间的频繁 reflow。
- requestAnimationFrame 合并:refreshLayout 使用 rAF 合并同一帧内的多次刷新请求,避免重复计算与重排。
- 样式优化:为 tbody 添加 will-change: transform,使 translateY 走合成器,降低每帧重绘开销。
flowchart TD
Start(["尺寸变化"]) --> Observe["ResizeObserver 回调"]
Observe --> Debounce{"是否已存在定时器?"}
Debounce -- 是 --> Clear["清除旧定时器"]
Debounce -- 否 --> Wait["等待 150ms"]
Clear --> Wait
Wait --> CheckSize{"width/height > 0 ?"}
CheckSize -- 否 --> End(["退出"])
CheckSize -- 是 --> Decide{"仅宽度变化?"}
Decide -- 是 --> RefreshX["initVirtualScrollX()"]
Decide -- 否 --> RefreshAll["initVirtualScroll()"]
RefreshX --> NextTick["nextTick"]
RefreshAll --> NextTick
NextTick --> End
图表来源
章节来源
StkTable 虚拟滚动核心算法(Y 轴)
- 可视范围计算:根据 scrollTop、rowHeight、headerRowHeight 计算 pageSize,进而得到 startIndex/endIndex。
- 自动行高与展开行:支持 autoRowHeight 与 expandConfig,通过缓存 Map 记录行高,避免重复测量。
- 合并单元格修正:对纵向合并行跨越可视区的边界进行修正,确保完整渲染。
- 实验性纵向滚动模式:isExperimentalScrollY 开启时,scrollTop 始终为 0,通过 translateY 移动内容,配合 will-change 提升性能。
flowchart TD
S(["updateVirtualScrollY"]) --> Read["读取 containerHeight/scrollTop"]
Read --> CalcPage["pageSize = ceil(containerHeight / rowHeight)"]
CalcPage --> AdjustHeader["减去表头占用的行数"]
AdjustHeader --> Bounds["maxScrollTop 校验并修正 scrollTop"]
Bounds --> Mode{"autoRowHeight/expand?"}
Mode -- 是 --> ScanRows["遍历累计高度确定 startIndex/endIndex"]
Mode -- 否 --> MathCalc["startIndex = floor(sTop / rowHeight)<br/>endIndex = startIndex + pageSize"]
ScanRows --> MergeFix["合并行跨越修正"]
MathCalc --> MergeFix
MergeFix --> Update["写入 startIndex/endIndex/offsetTop/translateY"]
Update --> End(["结束"])
图表来源
章节来源
StkTable 横向虚拟滚动(X 轴)与列合并修正
- 可视范围计算:基于 scrollLeft 与列宽累计数组,使用二分查找快速定位 startIndex,再累加至容器宽度确定 endIndex。
- 多级表头处理:按顶层组粒度过滤列,保证 colSpan 稳定,避免抖动。
- 列合并修正:收集可视区内列的 mergeCells 区间,扩展 startIndex/endIndex 以完整渲染合并单元格。
- 固定列与 spacer:分离 left/right 固定列,插入 spacer 项对齐表头与表体。
flowchart TD
SX(["updateVirtualScrollX"]) --> Cache["获取列宽缓存(nonFixedCols/leftFixedCols)"]
Cache --> FindStart{"sLeft > 0 ?"}
FindStart -- 是 --> BinSearch["二分查找 startIndex"]
FindStart -- 否 --> FirstCol["startIndex = nonFixedCols[0].index"]
BinSearch --> Offset["计算 offsetLeft 与 leftFirstColRestWidth"]
FirstCol --> Offset
Offset --> Accumulate["累加列宽至 containerWidth 确定 endIndex"]
Accumulate --> MergeRange["收集 mergeCells 区间并扩展范围"]
MergeRange --> FixedCols["分离 left/right 固定列"]
FixedCols --> Spacer["插入 spacer 对齐表头/表体"]
Spacer --> UpdateX["写入 startIndex/endIndex/offsetLeft/scrollLeft"]
UpdateX --> EndX(["结束"])
图表来源
- useVirtualScroll.ts:516-521
- useVirtualScroll.ts:719-789
- useVirtualScroll.ts:201-283
- useVirtualScroll.ts:344-443
章节来源
列宽调整(colResizable)的性能考虑
- 列宽变化会触发列宽缓存重建与 X 轴虚拟滚动参数更新,建议在批量调整列宽时使用防抖或批处理,避免频繁 reflow。
- 多级表头场景下,列宽变化可能影响顶层组过滤逻辑,需确保 spacer 与固定列对齐正确。
- 结合 FmsTable 的 ResizeObserver 防抖,可进一步降低连续调整过程中的重排次数。
章节来源
编辑态渲染路径优化
- 非编辑态:所有单元格走库原生 table-cell-wrapper 渲染路径,不注入 customCell,避免函数式组件重渲染。
- 进入编辑:仅活动编辑行挂载编辑器,其余行保持原生渲染,减少滚动时行间切换的重建成本。
- 键盘导航期间:长按方向键滚动全程保持零编辑器,松手后再激活最终目标行,避免反复创建/销毁组件导致卡顿。
章节来源
依赖关系分析
- FmsTable 依赖 stk-table-vue 的 StkTable 组件,并通过 props 控制虚拟滚动开关与列宽调整行为。
- useVirtualScroll 依赖 const.ts 中的默认常量(如 DEFAULT_TABLE_HEIGHT/WIDTH/ROW_HEIGHT)用于初始化与回退。
- 测试与测量工具验证虚拟滚动行为与性能指标。
graph LR
FT["FmsTable.vue"] --> ST["StkTable.vue"]
ST --> VS["useVirtualScroll.ts"]
VS --> C["const.ts"]
T["fms-table-virtual.spec.js"] --> FT
M["measure-wheel.mjs"] --> FT
图表来源
- FmsTable.vue:1-800
- useVirtualScroll.ts:1-814
- const.ts:1-51
- fms-table-virtual.spec.js:1-154
- measure-wheel.mjs:63-96
章节来源
性能考量
- 阈值控制:MIN_VIRTUAL_ROW_COUNT=100,低于该值关闭虚拟滚动,避免小数据量下的体验问题。
- 合成层优化:tbody 设置 will-change: transform,translateY 走合成器,减少重绘。
- 尺寸变化防抖:ResizeObserver 使用 150ms trailing 防抖,与库 autoResize 语义对齐,避免动画期间频繁重排。
- rAF 合并:refreshLayout 使用 requestAnimationFrame 合并同帧多次刷新,减少重排。
- 列宽缓存:useColWidthCache 避免每次滚动 O(n) 构建列宽累计数组,提升 X 轴滚动性能。
- 自动行高缓存:autoRowHeightMap 缓存行高,避免重复测量。
- 测量与回归:通过单元测试验证 DOM 渲染规模;通过 FPS 探针评估滚动帧率与渲染行数。
章节来源
- FmsTable.vue:41-96
- FmsTable.vue:613-616
- useVirtualScroll.ts:51-83
- useVirtualScroll.ts:529-559
- fms-table-virtual.spec.js:30-77
- measure-wheel.mjs:63-96
故障排查指南
- 虚拟滚动未生效:检查 effectiveVirtual 计算是否正确(dataSource.length 与 MIN_VIRTUAL_ROW_COUNT),确认 virtual 与 virtualX 属性传递。
- 滚动卡顿:确认 ResizeObserver 防抖是否正常工作,避免在动画期间频繁 initVirtualScroll;检查是否误用 auto-resize=true 导致与自定义刷新冲突。
- 列宽调整后错位:确认多级表头 spacer 与固定列对齐逻辑;必要时批量调整列宽并延迟刷新。
- 编辑器闪烁:确认键盘导航期间是否正确使用 pendingEditingTarget 与 scheduleEditActivation,避免长按滚动时频繁创建编辑器。
章节来源
结论
FMS 表格通过 FmsTable 与 StkTable 的协同,实现了高效稳定的虚拟滚动。通过阈值控制、合成层优化、rAF 合并与 ResizeObserver 防抖,有效降低了大数据量下的渲染与重排成本。X 轴虚拟滚动借助列宽缓存与列合并修正,保证了复杂表头与合并单元格的正确渲染。结合测试与测量工具,可在实际场景中持续验证与调优性能。
附录
- 配置建议
- 小数据量(<100 行):关闭虚拟滚动,全量渲染以获得更直观的用户体验。
- 中等数据量(100-1000 行):开启虚拟滚动,合理设置 row-height 与 header-row-height,避免频繁自动行高计算。
- 大数据量(>1000 行):开启虚拟滚动与 virtualX,禁用 auto-resize,使用自定义刷新时机;批量调整列宽后统一刷新。
- 最佳实践
- 使用 will-change: transform 提升滚动性能。
- 在动画或布局变化期间,使用防抖与 rAF 合并刷新。
- 避免在高频滚动路径中进行昂贵计算,优先使用缓存与增量更新。
- 通过单元测试与 FPS 探针持续验证渲染规模与帧率。