# 虚拟滚动优化 **本文引用的文件** - [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) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖关系分析](#依赖关系分析) 7. [性能考量](#性能考量) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本技术文档聚焦 FMS 表格系统的虚拟滚动能力,围绕 StkTable 库的虚拟滚动实现进行深度解析。内容涵盖可视区域计算、DOM 节点复用与内存管理、触发条件(MIN_VIRTUAL_ROW_COUNT 阈值)、性能优化策略(requestAnimationFrame 合并重排、ResizeObserver 防抖处理)、X 轴与 Y 轴虚拟滚动的差异,以及 colResizable 列宽调整时的性能考虑。文末提供不同数据量场景下的配置建议与最佳实践,帮助在大数据量渲染中取得稳定流畅的体验。 ## 项目结构 FMS 表格由上层 FmsTable 组件封装 stk-table-vue 提供的 StkTable 组件,并通过自定义逻辑控制虚拟滚动开关、尺寸变化刷新与编辑态渲染路径。测试用例验证了在不同数据规模下 DOM 节点数量保持在可视区范围内。 ```mermaid graph TB A["FmsTable.vue
封装与业务逻辑"] --> B["StkTable.vue
stk-table-vue 表格组件"] B --> C["useVirtualScroll.ts
Y/X 虚拟滚动核心"] B --> D["const.ts
默认常量/行高/尺寸"] E["fms-table-virtual.spec.js
虚拟滚动行为断言"] --> A F["measure-wheel.mjs
滚动帧率与渲染行数测量"] --> A ``` 图表来源 - [FmsTable.vue:1-800](file://fms-vue/src/components/fms-table/FmsTable.vue#L1-L800) - [useVirtualScroll.ts:1-814](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L1-L814) - [const.ts:1-51](file://fms-vue/node_modules/stk-table-vue/src/StkTable/const.ts#L1-L51) - [fms-table-virtual.spec.js:1-154](file://fms-vue/tests/fms-table-virtual.spec.js#L1-L154) - [measure-wheel.mjs:63-96](file://.fps-probe/measure-wheel.mjs#L63-L96) 章节来源 - [FmsTable.vue:1-800](file://fms-vue/src/components/fms-table/FmsTable.vue#L1-L800) - [package.json:21-34](file://fms-vue/package.json#L21-L34) ## 核心组件 - FmsTable 组件:负责虚拟滚动开关决策、尺寸变化监听与刷新、编辑态渲染路径优化、样式增强(合成层提升)。 - StkTable + useVirtualScroll:提供 Y 轴与 X 轴虚拟滚动核心算法,包括可视范围计算、列合并修正、固定列处理、自动行高缓存等。 - 测试与测量:通过单元测试验证 DOM 渲染规模;通过 FPS 探针评估滚动帧率与渲染行数。 章节来源 - [FmsTable.vue:10-76](file://fms-vue/src/components/fms-table/FmsTable.vue#L10-L76) - [useVirtualScroll.ts:99-175](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L99-L175) - [fms-table-virtual.spec.js:30-77](file://fms-vue/tests/fms-table-virtual.spec.js#L30-L77) - [measure-wheel.mjs:63-96](file://.fps-probe/measure-wheel.mjs#L63-L96) ## 架构总览 FmsTable 将虚拟滚动开关 effectiveVirtual 基于 MIN_VIRTUAL_ROW_COUNT 阈值动态计算,避免小数据量时因虚拟化导致的“吞掉最后一行”问题。StkTable 内部通过 useVirtualScroll 维护 Y 轴与 X 轴的可视范围、偏移量与分页大小,并在滚动事件中更新 startIndex/endIndex,仅渲染可见行列。 ```mermaid 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](file://fms-vue/src/components/fms-table/FmsTable.vue#L41-L68) - [useVirtualScroll.ts:479-521](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L479-L521) - [useVirtualScroll.ts:561-712](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L561-L712) - [useVirtualScroll.ts:719-789](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L719-L789) ## 详细组件分析 ### FmsTable 虚拟滚动开关与刷新机制 - 阈值控制:当数据行数超过 MIN_VIRTUAL_ROW_COUNT(100)时才启用虚拟滚动,避免小数据量下虚拟化带来的“最后几行不可见”问题。 - 尺寸变化处理:使用 ResizeObserver 监听容器尺寸变化,采用 trailing 防抖(约 150ms),仅在尺寸稳定后调用 initVirtualScroll/initVirtualScrollX,减少动画期间的频繁 reflow。 - requestAnimationFrame 合并:refreshLayout 使用 rAF 合并同一帧内的多次刷新请求,避免重复计算与重排。 - 样式优化:为 tbody 添加 will-change: transform,使 translateY 走合成器,降低每帧重绘开销。 ```mermaid 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 ``` 图表来源 - [FmsTable.vue:55-96](file://fms-vue/src/components/fms-table/FmsTable.vue#L55-L96) - [FmsTable.vue:613-616](file://fms-vue/src/components/fms-table/FmsTable.vue#L613-L616) 章节来源 - [FmsTable.vue:41-96](file://fms-vue/src/components/fms-table/FmsTable.vue#L41-L96) - [FmsTable.vue:613-616](file://fms-vue/src/components/fms-table/FmsTable.vue#L613-L616) ### StkTable 虚拟滚动核心算法(Y 轴) - 可视范围计算:根据 scrollTop、rowHeight、headerRowHeight 计算 pageSize,进而得到 startIndex/endIndex。 - 自动行高与展开行:支持 autoRowHeight 与 expandConfig,通过缓存 Map 记录行高,避免重复测量。 - 合并单元格修正:对纵向合并行跨越可视区的边界进行修正,确保完整渲染。 - 实验性纵向滚动模式:isExperimentalScrollY 开启时,scrollTop 始终为 0,通过 translateY 移动内容,配合 will-change 提升性能。 ```mermaid 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)
endIndex = startIndex + pageSize"] ScanRows --> MergeFix["合并行跨越修正"] MathCalc --> MergeFix MergeFix --> Update["写入 startIndex/endIndex/offsetTop/translateY"] Update --> End(["结束"]) ``` 图表来源 - [useVirtualScroll.ts:488-514](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L488-L514) - [useVirtualScroll.ts:561-712](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L561-L712) 章节来源 - [useVirtualScroll.ts:488-712](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L488-L712) ### StkTable 横向虚拟滚动(X 轴)与列合并修正 - 可视范围计算:基于 scrollLeft 与列宽累计数组,使用二分查找快速定位 startIndex,再累加至容器宽度确定 endIndex。 - 多级表头处理:按顶层组粒度过滤列,保证 colSpan 稳定,避免抖动。 - 列合并修正:收集可视区内列的 mergeCells 区间,扩展 startIndex/endIndex 以完整渲染合并单元格。 - 固定列与 spacer:分离 left/right 固定列,插入 spacer 项对齐表头与表体。 ```mermaid 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](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L516-L521) - [useVirtualScroll.ts:719-789](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L719-L789) - [useVirtualScroll.ts:201-283](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L201-L283) - [useVirtualScroll.ts:344-443](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L344-L443) 章节来源 - [useVirtualScroll.ts:516-789](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L516-L789) - [useVirtualScroll.ts:201-283](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L201-L283) - [useVirtualScroll.ts:344-443](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L344-L443) ### 列宽调整(colResizable)的性能考虑 - 列宽变化会触发列宽缓存重建与 X 轴虚拟滚动参数更新,建议在批量调整列宽时使用防抖或批处理,避免频繁 reflow。 - 多级表头场景下,列宽变化可能影响顶层组过滤逻辑,需确保 spacer 与固定列对齐正确。 - 结合 FmsTable 的 ResizeObserver 防抖,可进一步降低连续调整过程中的重排次数。 章节来源 - [useVirtualScroll.ts:51-83](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L51-L83) - [useVirtualScroll.ts:344-443](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L344-L443) - [FmsTable.vue:70-96](file://fms-vue/src/components/fms-table/FmsTable.vue#L70-L96) ### 编辑态渲染路径优化 - 非编辑态:所有单元格走库原生 table-cell-wrapper 渲染路径,不注入 customCell,避免函数式组件重渲染。 - 进入编辑:仅活动编辑行挂载编辑器,其余行保持原生渲染,减少滚动时行间切换的重建成本。 - 键盘导航期间:长按方向键滚动全程保持零编辑器,松手后再激活最终目标行,避免反复创建/销毁组件导致卡顿。 章节来源 - [FmsTable.vue:98-106](file://fms-vue/src/components/fms-table/FmsTable.vue#L98-L106) - [FmsTable.vue:373-448](file://fms-vue/src/components/fms-table/FmsTable.vue#L373-L448) - [fms-table-virtual.spec.js:79-127](file://fms-vue/tests/fms-table-virtual.spec.js#L79-L127) ## 依赖关系分析 - FmsTable 依赖 stk-table-vue 的 StkTable 组件,并通过 props 控制虚拟滚动开关与列宽调整行为。 - useVirtualScroll 依赖 const.ts 中的默认常量(如 DEFAULT_TABLE_HEIGHT/WIDTH/ROW_HEIGHT)用于初始化与回退。 - 测试与测量工具验证虚拟滚动行为与性能指标。 ```mermaid 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](file://fms-vue/src/components/fms-table/FmsTable.vue#L1-L800) - [useVirtualScroll.ts:1-814](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L1-L814) - [const.ts:1-51](file://fms-vue/node_modules/stk-table-vue/src/StkTable/const.ts#L1-L51) - [fms-table-virtual.spec.js:1-154](file://fms-vue/tests/fms-table-virtual.spec.js#L1-L154) - [measure-wheel.mjs:63-96](file://.fps-probe/measure-wheel.mjs#L63-L96) 章节来源 - [package.json:21-34](file://fms-vue/package.json#L21-L34) - [const.ts:1-51](file://fms-vue/node_modules/stk-table-vue/src/StkTable/const.ts#L1-L51) ## 性能考量 - 阈值控制: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](file://fms-vue/src/components/fms-table/FmsTable.vue#L41-L96) - [FmsTable.vue:613-616](file://fms-vue/src/components/fms-table/FmsTable.vue#L613-L616) - [useVirtualScroll.ts:51-83](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L51-L83) - [useVirtualScroll.ts:529-559](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L529-L559) - [fms-table-virtual.spec.js:30-77](file://fms-vue/tests/fms-table-virtual.spec.js#L30-L77) - [measure-wheel.mjs:63-96](file://.fps-probe/measure-wheel.mjs#L63-L96) ## 故障排查指南 - 虚拟滚动未生效:检查 effectiveVirtual 计算是否正确(dataSource.length 与 MIN_VIRTUAL_ROW_COUNT),确认 virtual 与 virtualX 属性传递。 - 滚动卡顿:确认 ResizeObserver 防抖是否正常工作,避免在动画期间频繁 initVirtualScroll;检查是否误用 auto-resize=true 导致与自定义刷新冲突。 - 列宽调整后错位:确认多级表头 spacer 与固定列对齐逻辑;必要时批量调整列宽并延迟刷新。 - 编辑器闪烁:确认键盘导航期间是否正确使用 pendingEditingTarget 与 scheduleEditActivation,避免长按滚动时频繁创建编辑器。 章节来源 - [FmsTable.vue:41-96](file://fms-vue/src/components/fms-table/FmsTable.vue#L41-L96) - [FmsTable.vue:373-448](file://fms-vue/src/components/fms-table/FmsTable.vue#L373-L448) - [useVirtualScroll.ts:344-443](file://fms-vue/node_modules/stk-table-vue/src/StkTable/useVirtualScroll.ts#L344-L443) ## 结论 FMS 表格通过 FmsTable 与 StkTable 的协同,实现了高效稳定的虚拟滚动。通过阈值控制、合成层优化、rAF 合并与 ResizeObserver 防抖,有效降低了大数据量下的渲染与重排成本。X 轴虚拟滚动借助列宽缓存与列合并修正,保证了复杂表头与合并单元格的正确渲染。结合测试与测量工具,可在实际场景中持续验证与调优性能。 ## 附录 - 配置建议 - 小数据量(<100 行):关闭虚拟滚动,全量渲染以获得更直观的用户体验。 - 中等数据量(100-1000 行):开启虚拟滚动,合理设置 row-height 与 header-row-height,避免频繁自动行高计算。 - 大数据量(>1000 行):开启虚拟滚动与 virtualX,禁用 auto-resize,使用自定义刷新时机;批量调整列宽后统一刷新。 - 最佳实践 - 使用 will-change: transform 提升滚动性能。 - 在动画或布局变化期间,使用防抖与 rAF 合并刷新。 - 避免在高频滚动路径中进行昂贵计算,优先使用缓存与增量更新。 - 通过单元测试与 FPS 探针持续验证渲染规模与帧率。