s_power 响应补授权,headed + 真实 GPU,含冻结克隆对照);②复刻探针页实测(headless,含 virtual 开/关判定与 errorCount 开关对比)。以①的数据为基准,②补充归因实验。两份的探针脚本均可复跑(见附录 B)。
editingActive 翻真 → displayColumns 23 个列对象全量重建 → 底座表头/几何/样式缓存全部失效 → 全表重排重绘(实测 298~394ms、966~1008 次 DOM 变更);换行点击 94~250ms(行级编辑态整行挂约 17 个编辑器);同行换列仅 0.7ms。updateFixedShadow 每次横滚都无条件写入新数组 → 固定列 class map 失效 → 全部单元格重渲染(与总行数/是否虚拟无关,与可见格数成正比)。基准口径:视口 1600×900,表格容器约 1308×656,rowHeight=28,默认 1000 行;帧率为滚动 3~5 秒窗口的中位数;真实页 + 真实 GPU(GTX 1050,帧率计数器口径)。
| 场景 | 帧率 / 耗时 | 最差帧 | 说明 |
|---|---|---|---|
| 纵向滚动 · 1000 行 | 15~17.4 FPS(≈58~67ms/帧) | ~600ms | 页面默认状态 |
| 纵向滚动 · 5000 行 | 15~17 FPS | ~600ms | 与总行数无关 |
| 纵向滚动 · 点开格子后(编辑态) | 8~11 FPS | — | customCell 10→17 列 + 选区 watcher(见 RC5/RC7) |
| 纵向滚动 · 10 / 100 行 | 54~60 FPS | — | ≤100 行门控自动关闭虚拟滚动 |
| 纵向滚动 · 克隆的静态 DOM | 60 FPS | — | 同一 DOM 同一 CSS,仅不挂 Vue |
| 横向滚动 · 1000 行 | 中位 20~26 FPS | 120~4800ms | fixed-col-shadow 开启 |
| 横向滚动 · 100 行 | 7.8 FPS | — | 2300 个 td 全量在 DOM,重渲染成本随之放大 |
| 首次点击某格 | 298~394ms | — | 966~1008 次 DOM 变更 |
| 换行点击(同列 / 换列) | 94~250ms | — | 约 70 次 DOM 变更(整行编辑器重挂) |
| 同一行换列点击 | 0.7ms | 0 次变更 | 仅编辑器重挂载一格 |
| 编辑提交(失焦)端到端 | 286ms@1000 / 465ms@5000 | — | 含 errorCount 全表重算 |
| 点「保存明细」 | 318ms@1000 / 612ms@5000 | — | 同上 |
| 纯打字(编辑器内输入) | 无明显长任务 | — | 草稿机制正常工作 |
| 数据量 | errorCount 计算耗时 | 换算 |
|---|---|---|
| 10 行 | 0.9~2.8ms | 约 7μs/格(每格跑一次 validate + 一次 checkPrecision),与行数严格线性 |
| 100 行 | 14~16ms | |
| 1000 行 | 31~69ms | |
| 5000 行 | 215~434ms |
triggerRef(virtualScroll) 触发的是单个 StkTable 组件的更新 → 表头 23 个 th + 可见约 25~31 行 × 23 列 ≈ 550~713 个格子的 props 重算、style 重写、keyed diff,外加 2 行 DOM 进出与占位高度变化。每次 window 变化都全量重做,无任何按行/按列的跳过。(program)(样式重算/布局/绘制),Vue JS 仅 ~7%——JS 便宜,贵在每帧 713 格的 style/layout/paint;④LoAF 直接指向底座的 rAF 回调。editingActive 翻真 → displayColumns 23 个列对象全部重建 → 底座 props.columns 引用变化 → handleDealColumns + updateMaxRowSpan + initVirtualScrollX + updateFixedShadow → tableHeaders 更换 → cellStyleMap 重建 → 全表重渲染 + 编辑器挂载 + focus。整条链没有任何 memo/短路。(program))。后续点击不重建列(editingActive 已为真),成本降至 94~250ms。editingActive 为真时该行每个可编辑列都挂编辑器(date 除外,已按格挂载)。一次换行点击 = 旧行 17 个卸载 + 新行 17 个挂载 + 状态类重写。errorCount 是 computed,依赖整个深层响应式 rows,每次失焦/回车/checkbox 提交都重跑「行数 × 22 列」,每格跑一次 validate + 一次 checkPrecision。选 20000 行时按线性外推约 +1.4s/次。validate + checkPrecision + 日期校验 + 数字格式化),且每格调用 useNamespace('table') 会 新建一个 computed 并 inject。滚动时每帧几百格都在做这两件事。createReactiveObject/refreshComputed 在点击与滚动中均可见。updateFixedShadow 在每次横滚中被调用,并无条件把新数组赋给 fixedCols / fixedShadowCols / BorderLeftCols 三个 shallowRef(即使内容毫无变化)→ fixedColClassMap computed 失效 → 所有 th/td 重渲染。与是否虚拟滚动无关,只与可见格数成正比。editingActive 翻真后 customCell 常驻列从 10(select/checkbox/validate/precision)涨到 17(text/number/date 加入),每格每帧走 displayValue(RC5)。vs.startIndex/endIndex → 滚动换窗即触发 updateSelectionDOM:清旧选区 + 逐行 querySelectorAll('td[data-col-key]') 遍历全部可见格、逐格生成 cellKey。选区点击后常驻(Esc、点表格外都不清除),此后每次滚动都付这笔钱。position:static(去 sticky)、去背景渐变、去 box-shadow、content-visibility,克隆全部保持 60fps。真实表格上改 CSS 同样收效甚微。CSS(sticky、29 条 td:has()、渐变格线)只影响「重渲染发生时每帧有多贵」,不影响「是否卡」。早前「67% 浏览器端开销」是每帧重渲染的后果,不是独立原因。位置:web/src/views/table-test/index.vue:615-625。
做法:从 computed 改为保存时才算——saveEdit() 里与 findFirstError() 合并成一次全表遍历(汇总数 + 首个错误一次拿到);工具栏徽章改为保存后刷新,或退而求其次只统计可见行 + 防抖。
收益:失焦 286→~90ms@1000、465→~90ms@5000;保存 318/612ms 同步下降。风险:无(徽标语义不变)。
位置:G3Table.vue:923(displayColumns 依赖 editingActive)。
顺序(从易到难,每步可独立上线):
1. 先用 Object.is 逐层比对确认到底哪一环翻转了列引用(防止改错地方);
2. 两态记忆化:按 editingActive 的真/假各缓存一份列数组,行与行之间切换不重建(现已在行间切换不翻转,缺的只是首击那次);
3. 最终让 customCell 注入不再依赖 editingActive:由 cell 内部 ctx.isEditing(row) 分发(函数本就接收 row),列引用从此稳定,点击不再触碰底座几何链。
收益:首次点击 298~394ms → 目标 ~50ms 以内。风险:中(需回归冻结列/列宽/编辑激活的 e2e)。
位置:stk-table-vue(上游库)或 G3Table 的 tbody 组织方式。
做法:把 tbody 行拆成独立子组件(每行一个渲染作用域),行数据不变的行跳过 patch;或对可见窗口做更细粒度的 diff 复用。这是 RC1 的正解,也是让纵向从 15~18 FPS 回到 60 FPS 量级的唯一路径。
收益:滚动卡顿整体解决(横向同步受益于 RC6 修复)。风险:高(触及底座渲染核心,需完整 e2e 回归:虚拟滚动、冻结列、合并单元格、树形、区域选择)。
位置:useAreaSelection.ts:319-342。
做法:watcher 去掉对 startIndex/endIndex 的依赖(滚动换窗时按需增量更新选区属性即可);选区为空时早退;把「逐格生成 cellKey 查 Set」换成按 selectedCellKeys 直接定位 td[data-col-key] 批量 setAttribute。
收益:点击后滚动 8~11 FPS → 15~18 FPS 档。风险:低-中(需回归拖选/Shift 扩选/复制)。
位置:utils/editable.ts:369-454(displayValue)、_composables/useNamespace.ts:41。
做法:①校验结果按 (dataIndex, value) 做值键缓存,注意跨列校验(如「目的港≠起运港」)的失效条件——按列声明依赖或按 (row 版本号 + value) 失效;②useNamespace 的 b/e/is 是纯字符串拼接,下沉为模块级单例,别在每格渲染里新建 computed。
收益:滚动每帧几百次 validate/checkPrecision 大幅收敛(参照 A/B:去 editor 后纵向 +38% 中的一部分)。风险:中(缓存失效条件是主要出错点,需单测覆盖跨列校验)。
位置:utils/column.ts:121-134 + utils/editable.ts:1177。
做法:date 已是「只给 focusCol 挂编辑器」的模式,把该模式推广到 text/number/select;非当前格走 displayValue(一个 span)。
收益:换行点击 94~250ms → 接近同行换列的 0.7ms 量级;编辑态滚动每帧少几百个函数式组件。风险:中(键盘连续录入 / Tab 跳格 / Enter 下移链路需 e2e 兜底)。
位置:style/_overrides.scss:460(恒常 sticky 补丁)+ useFixedCol.ts:157-164。
做法:A/B 只切 position、保留 z-index 与观感(避免「一起改混味道」);同时验证 RC6 机制——固定列 class map 无变化时跳过写入(先算字符串、有变化再赋值)。两个假设不互斥,用同一组实验一起验。
收益:横向 20~26 FPS → 60 FPS 量级;顺带消除 100 行区间的横滚塌陷。风险:低(观感回归为主)。
位置:G3Table.vue:1932-1941。无条件给约 28000px 高的 tbody 挂合成层;库自己仅在 .exp-scroll-y 下才加(stk-table-vue style.less:392-396)。与该条件对齐即可。实测单独去除改善有限(41→38ms),属低成本顺手项。
MIN_VIRTUAL_ROW_COUNT = 100 的门控是合理设计(小表示全量渲染更稳),不建议为性能调高——那只是把问题藏进「DOM 全量」。待 P1 修复落地后,该区间的横滚塌陷会随 RC6 一起消失,届时可重新评估阈值。
| 文件 | 行 | 内容 |
|---|---|---|
g3soft-libs/packages/ui/src/table/G3Table.vue | 251 | 纵向虚拟门控 MIN_VIRTUAL_ROW_COUNT = 100 |
| 584 | activateEditing / editingActive 翻转 | |
| 923 | displayColumns 全量重建(RC2 入口) | |
| 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:展示态每格跑校验(RC5) |
| 1177 | createEditableCell:行级编辑器分发(RC3) | |
g3soft-libs/packages/ui/src/_composables/useNamespace.ts | 41 | 每格渲染新建一个 computed(RC5) |
g3soft-libs/packages/ui/src/table/style/_overrides.scss | 460 | fixed-cell 恒常 sticky 补丁(A/B 对象) |
stk-table-vue/src/StkTable/StkTable.vue | 1240 | columns 变化 → 全量重算链(RC2) |
| 1927 | onTableScroll:rAF 节流的滚动入口(RC1) | |
| 1945 | 横滚分支调用 updateFixedShadow(RC6) | |
stk-table-vue/src/StkTable/useFixedCol.ts | 157 | 每次横滚无条件写新数组 → class map 失效(RC6) |
stk-table-vue/src/StkTable/features/useAreaSelection.ts | 319 | 选区 watcher 依赖 startIndex/endIndex(RC7b) |
| 217 | updateSelectionDOM 全可见格遍历(RC7b) | |
web/src/views/table-test/index.vue | 615 | errorCount computed(RC4) |
两份排查的探针脚本均为只读(不改业务代码、不请求业务接口、可随时删):
g3soft-libs/packages/ui/probe-table-*.mjs,如 perf(基线)、gpu(三路径对照)、churn(DOM 变更计数)、final(克隆 + A/B)、ab(观感 A/B)。用法示例:node probe-table-perf.mjs --gpu=none --case=vertical-1000 --seconds=5 --rows=5 --repeat=3 --json。运行前置:前端 5090 与后端 8088 已起。web/__perf-table.html + 系列脚本做 virtual 开/关与 errorCount 开关对比,结论已并入本报告;如需复现按附录 A 的列定义重建即可。demo_zhangwei 访问 /table-test 会 403(该账号只授了 menu.approval% 与 action.v_b_othercompany.%)。sql/20_registration/13_table_test_menu.sql 只建菜单、不建权限点——要普通账号可进,需执行其末尾「可选:给普通账号授权」段;或直接用 g3soft。
pnpm build 产物)下的表现未验证;dev 模式有额外开销,但不影响根因归属。