editingActive 翻真,触发全量列重建(23 个新列对象 → 列宽/样式缓存重建 → 全表重排重绘),实测主线程阻塞约 400~550ms;后续点击约 130~200ms。errorCount computed 每次提交都全量重跑「行数 × 22 列」校验,1000 行多付 ~60ms、5000 行多付 ~190ms,20000 行约 +0.8s/次。demo_zhangwei 没有 /table-test 菜单权限(访问会被守卫送到 /403),因此在 Vite dev server 下搭建了复刻探针页:同样的 22 列定义、rowHeight=28、1000 行同分布假数据、同样的 fixed-col-shadow 与校验规则,排除 ERP 外壳干扰。requestAnimationFrame 采样帧间隔;PerformanceObserver 抓 longtask;CDP Profiler 抓 CPU 热点(自底向上 self time 聚合)。div 5000 行滚动(浏览器基线,满帧 60fps,验证测法可靠);纯展示列表格;完整编辑配置表格;virtual on/off 对比。| 场景 | 平均帧 | p50 | p95 | 卡顿帧(>32ms) |
|---|---|---|---|---|
| 纵向滚动,virtual on(页面默认 1000 行) | 34~75ms(13~29 FPS) | 33~100ms | 83~167ms | 30~41 / 约 60 帧 |
| 纵向滚动,点击过单元格后(选区常驻) | 74.8ms(13 FPS) | 99.9ms | 166.7ms | 19 / 35 |
| 横向滚动(fixed-col-shadow 开) | 19.9ms | 16.7ms | 33.4ms | 22 |
| 同样 1000 行,virtual off(全量 23000 td,滚动零重渲染) | 28ms | 16.7ms | 116.6ms | 23 |
| 100 行(门控自动关闭虚拟) | 16.7ms(60 FPS) | 16.7ms | 16.7ms | 0 |
| 裸 div 5000 行(浏览器基线) | 16.7ms(60 FPS) | 16.7ms | 16.8ms | 0 |
onTableScroll(rAF 节流)→ updateVirtualScrollY 换窗 → triggerRef(virtualScroll) → 整个 StkTable 组件重渲染:表头 23 个 th + 可见 31 行 × 23 列 ≈ 713 个单元格的 props 重算与 diff,外加 2 行 DOM 进出。(program)(样式重算/布局/绘制),Vue JS 仅 ~7%,GC 2.9%,setProperty(Vue 写 style)4%。即贵在每帧 713 格的 style/layout/paint。.g3-table__inner .stk-tbody-main { will-change: transform }(G3Table.vue:1936)——无条件给约 28000px 高的 tbody 挂合成层;库自己只在 transform 滚动模式(.exp-scroll-y)下才加(stk-table-vue/src/StkTable/style.less:392),本页走原生滚动,这层纯属白付(实测去掉仅小幅改善,属次要因素)。position: sticky 格(补丁让所有 fixed-cell 恒常 sticky,style/_overrides.scss:460)。td:has(...) 规则(编辑器/校验/只读态反查)+ 库用背景渐变画格线。virtual=false(一次渲染 23000 个 td)→ 滚动 p50 回到 16.7ms 满帧。证明卡的是「虚拟滚动开启后的每帧重渲染」,而非 DOM 总量。editingActive 翻真后,注入 customCell 的列从 10 个(select/checkbox/validate/precision 常驻)涨到 17 个(text/number/date 全部加入,utils/column.ts:121-134),每格每帧走 displayValue:跑业务 validate + checkPrecision + 日期校验 + 数字格式化(utils/editable.ts:369-454)。vs.startIndex/endIndex → 滚动换窗即触发 updateSelectionDOM:先 querySelectorAll 清旧选区,再逐行 querySelectorAll('td[data-col-key]') 遍历全部可见格、逐格生成 cellKey 并查 Set。选区在点击后常驻(Esc、点表格外都不清除,实测点击后选区数仍为 1),此后每次滚动都付这笔钱。| 操作 | longtask 序列 | 随后最大帧 | 估算阻塞 |
|---|---|---|---|
| 首次点击某可编辑格 | 137ms → 389ms | 416.7ms | ~400~550ms |
| 点另一行同列 | 92ms → 136ms | 166.7ms | ~230~300ms |
| 行内换到另一列 | 130ms → 121ms | 133.3ms | ~250ms |
| 点只读列(订舱号) | 97ms → 198ms | 216.6ms | ~300ms |
编辑器聚焦本身很快(mousedown 后 ~0.3ms 即聚焦),卡的是点击引发的重渲染/重排链条。
handleAreaSelectionChange → activateEditing → editingActive 翻真(G3Table.vue:584-609)。displayColumns 全量重建:23 个列全部 map 成新对象(G3Table.vue:923-1004)。props.columns 引用变化 → watch → handleDealColumns + updateMaxRowSpan → nextTick → initVirtualScrollX + updateFixedShadow + updateCustomScrollbar(StkTable.vue:1240-1252)。tableHeaders 更换 → cellStyleMap 重建 → 全表重渲染 + 编辑器挂载 + focus()。CPU profile:主要成本仍在浏览器端(364ms (program),即全表 style/layout/paint);focus() 本身占 40~57ms。editingActive 已为真),只剩行编辑态切换 + 选区 DOM 更新 + setCurrentRow,故成本降至 ~130~200ms。views/table-test/index.vue:615-625 的 errorCount 是 computed,依赖整个深层响应式 rows,每次提交(Enter / 失焦 / checkbox)都全量重跑「行数 × 22 列」校验(每格还要跑 validate + checkPrecision)。实测 Enter 提交耗时:
| 数据量 | errorCount 开 | errorCount 关 | 差值 |
|---|---|---|---|
| 1000 行 | 139ms | 79ms | +60ms |
| 5000 行 | 278ms | 85ms | +193ms |
按线性外推,选 20000 行时每次提交约多付 ~0.8s。这是页面级问题,与组件库无关;工具栏徽章只在打开页面/保存时有意义,不必随每次编辑重算。
位置:stk-table-vue(node_modules,属上游库)或 G3Table.vue 的 tbody 渲染组织方式。
问题:triggerRef(virtualScroll) 一触发,整个 StkTable 单组件重渲染,713 个单元格 props 全量重算与 diff。
方向:把 tbody 行拆成独立子组件(每行一个渲染作用域),行数据不变时跳过该行 patch;或对可见窗口做更细的 keyed diff 复用。virtual off 满帧已证明 DOM 量不是问题,问题是每帧全量重算。
位置:G3Table.vue:1932-1941(SCSS 块,非 scoped)。
问题:原生滚动模式下也给 28000px 高的 tbody 常驻合成层;库自己仅在 .exp-scroll-y 下才加(style.less:392-396)。
方向:与该条件对齐,仅在 transform 滚动模式输出此规则;或直接删除(transform 模式由库自己加)。实测单独去除改善有限(41→38ms),属低成本顺手项。
位置:stk-table-vue/src/StkTable/features/useAreaSelection.ts:319-342。
问题:watcher 依赖 startIndex/endIndex,滚动换窗即对全部可见格做 DOM 遍历 + cellKey 生成;选区常驻导致每次滚动都付。
方向:选区为空时早退;把「逐格生成 cellKey 查 Set」改为按 selectedCellKeys 直接定位 td[data-col-key] 批量 setAttribute;或只在选区真正变化时重算,滚动换窗时按需增量更新。
位置:G3Table.vue:923(displayColumns computed 依赖 editingActive)。
问题:首次点击 editingActive 翻真 → 23 个列对象全部重建 → 列宽/样式缓存重建 → 全表重排重绘(~400ms)。
方向:编辑态从「全表列级」下沉为「行级」——只给目标行/目标列注入编辑器(customCell 内部已有 ctx.isEditing(row) 判断,具备按行分发的基礎);或缓存列对象、仅对受影响的列生成新引用,避免 tableHeaders/cellStyleMap 整体失效。
位置:web/src/views/table-test/index.vue:615-625。
问题:每次提交全量重跑 行数×22 校验,1000 行 +60ms、5000 行 +193ms、20000 行约 +0.8s。
方向:改为在 saveEdit() 里按需算一次(保存时本就要全表校验,可与 findFirstError 合并成一次遍历);或只统计可见行 + 防抖。徽标语义不变。
位置:utils/editable.ts:369-454(displayValue → resolveEditorStatus)。
问题:滚动时每格每帧重跑 validate + checkPrecision + 日期校验 + 格式化;点击后常驻 customCell 从 10 列涨到 17 列,成本随之上涨。
方向:校验结果是行数据的派生值,可按 (row, dataIndex, value) 做 WeakMap 缓存,值不变则复用;useNamespace 每格渲染都新建 computed(_composables/useNamespace.ts:41),可下沉为模块级单例。
位置:G3Table.vue:251(MIN_VIRTUAL_ROW_COUNT = 100)。
说明:阈值本身合理(小表示全量渲染更稳),不建议为性能调高——那只是把问题藏到「DOM 全量」里,万行级内存与首渲染会更差。应优先修 P1,让虚拟滚动本身变快。
| 文件 | 行 | 内容 |
|---|---|---|
g3soft-libs/packages/ui/src/table/G3Table.vue | 251 | 纵向虚拟门控 MIN_VIRTUAL_ROW_COUNT = 100 |
| 584 | activateEditing / editingActive 翻转 | |
| 923 | displayColumns 全量重建(点击卡顿入口) | |
| 1207 | handleAreaSelectionChange(点击 → 进编辑链路) | |
| 1932 | tbody 无条件 will-change: transform | |
g3soft-libs/packages/ui/src/table/utils/column.ts | 121 | customCell 注入判据(editingActive 后 10 → 17 列) |
g3soft-libs/packages/ui/src/table/utils/editable.ts | 369 | displayValue:展示态每格跑校验 |
g3soft-libs/packages/ui/src/table/style/_overrides.scss | 460 | fixed-cell 恒常 sticky 补丁 |
stk-table-vue/src/StkTable/StkTable.vue | 1240 | columns 变化 → 全量重算链 |
stk-table-vue/src/StkTable/StkTable.vue | 1927 | onTableScroll(rAF 节流的滚动入口) |
stk-table-vue/src/StkTable/features/useAreaSelection.ts | 319 | 选区 watcher 依赖 startIndex/endIndex |
stk-table-vue/src/StkTable/features/useAreaSelection.ts | 217 | updateSelectionDOM 全可见格遍历 |
web/src/views/table-test/index.vue | 615 | errorCount computed(页面级提交卡顿) |