Files
workspace/code/g3soft-erp/web/table-test-性能排查报告-终版.html
T
2026-10-16 17:30:17 +08:00

313 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>/table-test 表格性能排查报告(终版)</title>
<style>
:root {
--text: #1f2329;
--text-2: #4e5969;
--text-3: #86909c;
--border: #e5e6eb;
--bg: #f7f8fa;
--card: #ffffff;
--primary: #165dff;
--danger: #ec4246;
--warning: #e79a0d;
--ok: #00b42a;
--code-bg: #f2f3f5;
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 40px 24px 80px;
background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px; line-height: 1.75;
}
.page { max-width: 960px; margin: 0 auto; }
header.doc { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 28px 32px; margin-bottom: 20px; }
header.doc h1 { margin: 0 0 8px; font-size: 22px; }
header.doc .meta { color: var(--text-3); font-size: 13px; }
header.doc .meta span { margin-right: 18px; }
section { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 24px 32px; margin-bottom: 20px; }
h2 { font-size: 17px; margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
h3 { font-size: 15px; margin: 22px 0 8px; }
p { margin: 8px 0; }
ul, ol { margin: 8px 0; padding-left: 24px; }
li { margin: 4px 0; }
code { background: var(--code-bg); border-radius: 4px; padding: 1px 6px; font-family: Consolas, Monaco, "Courier New", monospace; font-size: 12.5px; color: #c0341d; word-break: break-all; }
.file { color: var(--primary); }
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
th, td { border: 1px solid var(--border); padding: 7px 12px; text-align: left; vertical-align: top; }
th { background: #f2f3f5; font-weight: 600; white-space: nowrap; }
td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.bad { color: var(--danger); font-weight: 600; }
.good { color: var(--ok); font-weight: 600; }
.warn { color: var(--warning); font-weight: 600; }
.tldr { background: #f0f5ff; border: 1px solid #bedaff; border-radius: 8px; padding: 14px 18px; margin: 12px 0; }
.tldr ol { margin: 6px 0 0; }
.note { background: #fff7e8; border: 1px solid #f5d8a8; border-radius: 8px; padding: 12px 16px; margin: 12px 0; font-size: 13px; color: #6b5300; }
.rc { border: 1px solid var(--border); border-left: 4px solid var(--danger); border-radius: 8px; padding: 14px 18px; margin: 12px 0; }
.rc.r2 { border-left-color: var(--warning); }
.rc.r3 { border-left-color: var(--primary); }
.rc h4 { margin: 0 0 6px; font-size: 14.5px; }
.rc .ev { font-size: 13px; color: var(--text-2); margin: 6px 0; }
.rc .ev b { color: var(--text); }
.fix { border: 1px solid var(--border); border-left: 4px solid var(--primary); border-radius: 8px; padding: 14px 18px; margin: 12px 0; }
.fix.p0 { border-left-color: var(--danger); }
.fix.p2 { border-left-color: var(--ok); }
.fix h4 { margin: 0 0 6px; font-size: 14.5px; }
.fix .tag { display: inline-block; font-size: 12px; font-weight: 600; border-radius: 4px; padding: 0 8px; margin-right: 8px; vertical-align: 1px; }
.tag.high { background: #ffece8; color: var(--danger); }
.tag.mid { background: #fff7e8; color: var(--warning); }
.tag.low { background: #e8ffea; color: var(--ok); }
.fix p { margin: 6px 0; color: var(--text-2); font-size: 13.5px; }
.kv { color: var(--text-3); font-size: 12.5px; }
.no { color: var(--text-2); }
.no li { margin: 6px 0; }
footer { color: var(--text-3); font-size: 12.5px; text-align: center; margin-top: 8px; }
</style>
</head>
<body>
<div class="page">
<header class="doc">
<h1>/table-test 表格性能排查报告(终版)</h1>
<div class="meta">
<span>日期:2026-10-16</span>
<span>对象:<code>web/src/views/table-test/index.vue</code> + <code>@g3soft/ui</code> G3Table(底座 stk-table-vue 1.2.7)</span>
<span>状态:根因已定位,未改代码</span>
</div>
<div class="kv" style="margin-top:10px">
本终版由两份独立排查合并去重而成:①真实页面实测(测试进程内拦截 <code>s_power</code> 响应补授权,headed + 真实 GPU,含冻结克隆对照);②复刻探针页实测(headless,含 virtual 开/关判定与 errorCount 开关对比)。以①的数据为基准,②补充归因实验。两份的探针脚本均可复跑(见附录 B)。
</div>
</header>
<section>
<h2>摘要(TL;DR)</h2>
<div class="tldr">
<ol>
<li><b>滚动卡的唯一根因</b>:数据量 &gt; 100 行时纵向虚拟滚动开启,每滚一步触发<b>整个表格组件重渲染</b>(约 550 个可见格的 props 重算 + style 重写 + 行窗口 DOM 进出)。冻结克隆实验证明:同一份 DOM、同一套 CSS,不触发重渲染就是满帧 60fps——<b>CSS 不是根因,是重渲染发生时放大每帧成本的因素</b>。</li>
<li><b>点击卡的主因</b>:首次点击使 <code>editingActive</code> 翻真 → <code>displayColumns</code> 23 个列对象全量重建 → 底座表头/几何/样式缓存全部失效 → 全表重排重绘(实测 298~394ms、966~1008 次 DOM 变更);换行点击 94~250ms(行级编辑态整行挂约 17 个编辑器);同行换列仅 0.7ms。</li>
<li><b>点击后滚动进一步恶化</b>(纵向 15~18 FPS → 8~11 FPS):常驻 customCell 的列从 10 涨到 17;且底座选区 watcher 依赖滚动窗口索引,每次滚动都全量遍历可见格更新选区 DOM。</li>
<li><b>横向滚动</b>中位 20~26 FPS、100 行时恶化到 7.8 FPS:<code>updateFixedShadow</code> 每次横滚都无条件写入新数组 → 固定列 class map 失效 → 全部单元格重渲染(与总行数/是否虚拟无关,与可见格数成正比)。</li>
<li><b>页面自身的 errorCount</b> 每次提交全量重跑「行数 × 22 列」校验,严格线性(7μs/格):1000 行 +31~69ms、5000 行 +215~434ms;失焦端到端 286ms@1000 / 465ms@5000,点保存 318ms / 612ms。</li>
<li>已排除:GPU/软件渲染无差别、行数不是直接因素、列宽拖拽零开销、关闭虚拟滚动不可取(23000 DOM、6.1s 挂载)。</li>
</ol>
</div>
</section>
<section>
<h2>一、性能基线(实测)</h2>
<p class="kv">基准口径:视口 1600×900,表格容器约 1308×656,rowHeight=28,默认 1000 行;帧率为滚动 3~5 秒窗口的中位数;真实页 + 真实 GPU(GTX 1050,帧率计数器口径)。</p>
<table>
<tr><th>场景</th><th>帧率 / 耗时</th><th>最差帧</th><th>说明</th></tr>
<tr><td>纵向滚动 · 1000 行</td><td class="num bad">15~17.4 FPS(≈58~67ms/帧)</td><td class="num">~600ms</td><td>页面默认状态</td></tr>
<tr><td>纵向滚动 · 5000 行</td><td class="num bad">15~17 FPS</td><td class="num">~600ms</td><td><b>与总行数无关</b></td></tr>
<tr><td>纵向滚动 · 点开格子后(编辑态)</td><td class="num bad">8~11 FPS</td><td class="num">—</td><td>customCell 10→17 列 + 选区 watcher(见 RC5/RC7)</td></tr>
<tr><td>纵向滚动 · 10 / 100 行</td><td class="num good">54~60 FPS</td><td class="num good">—</td><td>≤100 行门控自动关闭虚拟滚动</td></tr>
<tr><td>纵向滚动 · 克隆的静态 DOM</td><td class="num good">60 FPS</td><td class="num good">—</td><td>同一 DOM 同一 CSS,仅不挂 Vue</td></tr>
<tr><td>横向滚动 · 1000 行</td><td class="num warn">中位 20~26 FPS</td><td class="num bad">120~4800ms</td><td>fixed-col-shadow 开启</td></tr>
<tr><td>横向滚动 · 100 行</td><td class="num bad">7.8 FPS</td><td class="num">—</td><td>2300 个 td 全量在 DOM,重渲染成本随之放大</td></tr>
<tr><td>首次点击某格</td><td class="num bad">298~394ms</td><td class="num">—</td><td>966~1008 次 DOM 变更</td></tr>
<tr><td>换行点击(同列 / 换列)</td><td class="num warn">94~250ms</td><td class="num">—</td><td>约 70 次 DOM 变更(整行编辑器重挂)</td></tr>
<tr><td>同一行换列点击</td><td class="num good">0.7ms</td><td class="num good">0 次变更</td><td>仅编辑器重挂载一格</td></tr>
<tr><td>编辑提交(失焦)端到端</td><td class="num warn">286ms@1000 / 465ms@5000</td><td class="num">—</td><td>含 errorCount 全表重算</td></tr>
<tr><td>点「保存明细」</td><td class="num warn">318ms@1000 / 612ms@5000</td><td class="num">—</td><td>同上</td></tr>
<tr><td>纯打字(编辑器内输入)</td><td class="num good">无明显长任务</td><td class="num good">—</td><td>草稿机制正常工作</td></tr>
</table>
<h3>errorCount 线性标定(另一处页面级问题)</h3>
<table>
<tr><th>数据量</th><th>errorCount 计算耗时</th><th>换算</th></tr>
<tr><td>10 行</td><td class="num">0.9~2.8ms</td><td rowspan="4">约 7μs/格(每格跑一次 <code>validate</code> + 一次 <code>checkPrecision</code>),与行数严格线性</td></tr>
<tr><td>100 行</td><td class="num">14~16ms</td></tr>
<tr><td>1000 行</td><td class="num">31~69ms</td></tr>
<tr><td>5000 行</td><td class="num">215~434ms</td></tr>
</table>
</section>
<section>
<h2>二、根因链(去重合并,按影响排序)</h2>
<div class="rc">
<h4>RC1 · 虚拟滚动开启后,每滚一步全表重渲染 <span class="kv">(滚动卡的根子)</span></h4>
<div class="ev"><b>机制</b>:<code>triggerRef(virtualScroll)</code> 触发的是<b>单个 StkTable 组件</b>的更新 → 表头 23 个 th + 可见约 25~31 行 × 23 列 ≈ <b>550~713 个格子的 props 重算、style 重写、keyed diff</b>,外加 2 行 DOM 进出与占位高度变化。每次 window 变化都全量重做,无任何按行/按列的跳过。</div>
<div class="ev"><b>证据</b>:①冻结克隆(同一 DOM 滚动 60fps vs live 15fps);②virtual 开/关、10/100 行、5000 行帧率一致;③CDP profile 中 67% 时间在浏览器端 <code>(program)</code>(样式重算/布局/绘制),Vue JS 仅 ~7%——JS 便宜,贵在每帧 713 格的 style/layout/paint;④LoAF 直接指向底座的 rAF 回调。</div>
<div class="ev"><b>位置</b>:<span class="file">stk-table-vue/src/StkTable/StkTable.vue:1927</span>(onTableScroll)、<span class="file">useVirtualScroll.ts:140-148</span>(窗口变化即 triggerRef)。</div>
</div>
<div class="rc r2">
<h4>RC2 · 首次点击触发「列重建级联」,全表重排重绘 <span class="kv">(点击卡的主因)</span></h4>
<div class="ev"><b>机制</b>:mousedown → 选区变化 → <code>editingActive</code> 翻真 → <code>displayColumns</code> 23 个列对象<b>全部重建</b> → 底座 <code>props.columns</code> 引用变化 → <code>handleDealColumns</code> + <code>updateMaxRowSpan</code> + <code>initVirtualScrollX</code> + <code>updateFixedShadow</code> → <code>tableHeaders</code> 更换 → <code>cellStyleMap</code> 重建 → 全表重渲染 + 编辑器挂载 + focus。整条链没有任何 memo/短路。</div>
<div class="ev"><b>证据</b>:首次点击 298~394ms / 966~1008 次 DOM 变更(另一复刻页实测:137ms + 389ms 两个 longtask + 一个 416ms 帧);点击的 CPU profile 同样显示主要成本在浏览器端(364ms <code>(program)</code>)。后续点击不重建列(editingActive 已为真),成本降至 94~250ms。</div>
<div class="ev"><b>位置</b>:<span class="file">G3Table.vue:584</span>(activateEditing)、<span class="file">G3Table.vue:923</span>(displayColumns)、<span class="file">StkTable.vue:1240</span>(columns watch)。</div>
</div>
<div class="rc r2">
<h4>RC3 · 行级编辑态:一行点亮,整行约 17 个编辑器全部挂载</h4>
<div class="ev"><b>机制</b>:<code>editingActive</code> 为真时该行<b>每个</b>可编辑列都挂编辑器(date 除外,已按格挂载)。一次换行点击 = 旧行 17 个卸载 + 新行 17 个挂载 + 状态类重写。</div>
<div class="ev"><b>证据</b>:编辑器根节点数 24 → 41~42(一次点击净增约 17 个);换行点击 94~187ms / 约 70 次 DOM 变更。对照组:同一行换列只重挂一格,0.7ms / 0 次 DOM 变更。</div>
<div class="ev"><b>位置</b>:<span class="file">utils/column.ts:121-134</span>(注入判据)、<span class="file">utils/editable.ts:1177</span>(createEditableCell 按行分发)。</div>
</div>
<div class="rc r2">
<h4>RC4 · 页面 errorCount:每次提交全表重跑校验</h4>
<div class="ev"><b>机制</b>:<code>errorCount</code> 是 computed,依赖整个深层响应式 <code>rows</code>,每次失焦/回车/checkbox 提交都重跑「行数 × 22 列」,每格跑一次 <code>validate</code> + 一次 <code>checkPrecision</code>。选 20000 行时按线性外推约 +1.4s/次。</div>
<div class="ev"><b>证据</b>:线性标定见上表;复刻页 A/B:Enter 提交 139ms vs 79ms(1000 行)、278ms vs 85ms(5000 行)。</div>
<div class="ev"><b>位置</b>:<span class="file">web/src/views/table-test/index.vue:615-625</span>。</div>
</div>
<div class="rc r3">
<h4>RC5 · 每格渲染的重复劳动:校验重算 + 每格新建一个 computed</h4>
<div class="ev"><b>机制</b>:customCell 每次渲染都重新求校验态(业务 <code>validate</code> + <code>checkPrecision</code> + 日期校验 + 数字格式化),且每格调用 <code>useNamespace('table')</code> 会 <b>新建一个 computed</b> 并 inject。滚动时每帧几百格都在做这两件事。</div>
<div class="ev"><b>证据</b>:A/B 去掉全部 editor 后纵向 18.2 → 25.2 FPS(+38%)、横向 32.2 → 43.3 FPS(+34%);profile 中 <code>createReactiveObject</code>/<code>refreshComputed</code> 在点击与滚动中均可见。</div>
<div class="ev"><b>位置</b>:<span class="file">utils/editable.ts:369-454</span>(displayValue)、<span class="file">_composables/useNamespace.ts:41</span>。</div>
</div>
<div class="rc r2">
<h4>RC6 · 横向滚动:固定列 class map 每帧失效导致全量重渲染</h4>
<div class="ev"><b>机制</b>:<code>updateFixedShadow</code> 在<b>每次横滚</b>中被调用,并无条件把<b>新数组</b>赋给 <code>fixedCols</code> / <code>fixedShadowCols</code> / <code>BorderLeftCols</code> 三个 shallowRef(即使内容毫无变化)→ <code>fixedColClassMap</code> computed 失效 → 所有 th/td 重渲染。与是否虚拟滚动无关,只与可见格数成正比。</div>
<div class="ev"><b>证据</b>:100 行(2300 格全量在 DOM、纵向因不换窗而 58.8 FPS)横滚却只有 7.8 FPS;1000 行(约 552 格)横滚中位 20~26 FPS。格数比 4.2×、帧时间比约 2.8×,方向吻合。另一复刻页在无选区状态下实测横滚接近满帧(p50 16.7ms)——差异归于选区/编辑态与页面外壳,以真实页数据为准。</div>
<div class="ev"><b>位置</b>:<span class="file">stk-table-vue/src/StkTable/features/useFixedCol.ts:157-164</span>(无条件写新数组)、<span class="file">StkTable.vue:1945-1952</span>(横滚即调用)。</div>
</div>
<div class="rc r2">
<h4>RC7 · 点击之后滚动更卡:常驻编辑列增多 + 选区 watcher 每帧全量跑</h4>
<div class="ev"><b>机制 a</b>:<code>editingActive</code> 翻真后 customCell 常驻列从 10(select/checkbox/validate/precision)涨到 17(text/number/date 加入),每格每帧走 displayValue(RC5)。<br /><b>机制 b</b>:底座选区 watcher <b>依赖 <code>vs.startIndex/endIndex</code></b> → 滚动换窗即触发 <code>updateSelectionDOM</code>:清旧选区 + 逐行 <code>querySelectorAll('td[data-col-key]')</code> 遍历全部可见格、逐格生成 cellKey。选区点击后<b>常驻</b>(Esc、点表格外都不清除),此后每次滚动都付这笔钱。</div>
<div class="ev"><b>证据</b>:编辑态纵向 8~11 FPS(vs 未点击 15~18);复刻页实测点击后滚动 44.1ms → 74.8ms/帧。</div>
<div class="ev"><b>位置</b>:<span class="file">useAreaSelection.ts:319-342</span>(watcher 依赖滚动窗口)、<span class="file">useAreaSelection.ts:217</span>(updateSelectionDOM)。</div>
</div>
</section>
<section>
<h2>三、已排除的嫌疑(避免重复踩坑)</h2>
<ul class="no">
<li><b>CSS 不是根因</b>:冻结克隆 A/B/C/D——对克隆 DOM 逐一加 <code>position:static</code>(去 sticky)、去背景渐变、去 box-shadow、<code>content-visibility</code>,克隆全部保持 60fps。真实表格上改 CSS 同样收效甚微。CSS(sticky、29 条 <code>td:has()</code>、渐变格线)只影响「重渲染发生时每帧有多贵」,不影响「是否卡」。早前「67% 浏览器端开销」是每帧重渲染的<b>后果</b>,不是独立原因。</li>
<li><b>GPU / 软件渲染无差别</b>:headless、强制 D3D11、headed+GTX 1050 三条路径帧率几乎一致。问题在主线程的每帧工作量,不在合成。</li>
<li><b>与总行数无关</b>:1000 与 5000 行同为 15~17 FPS;成本只与可见格数有关。</li>
<li><b>列宽拖拽开销为零</b>:持续拖拽期间零掉帧、零长任务。</li>
<li><b>关闭虚拟滚动不可取</b>:1000 行全量渲染 = 23000 个 DOM、6.1s 挂载、内存 70→120MB。滚动满帧是以巨大的固定成本换来的,不能作为修复方向。</li>
<li><b>100 行是「两头不讨好」区间</b>:纵向 54~60 FPS(虚拟已关)但横向 7.8 FPS(RC6 与行数无关地全量重渲染)。属已知边界,记录即可。</li>
</ul>
</section>
<section>
<h2>四、修改方向(合并去重,按优先级)</h2>
<div class="fix p0">
<h4><span class="tag high">P0 · 页面级 · 零风险</span> errorCount 改为按需计算</h4>
<p><b>位置</b>:<code>web/src/views/table-test/index.vue:615-625</code>。</p>
<p><b>做法</b>:从 computed 改为保存时才算——<code>saveEdit()</code> 里与 <code>findFirstError()</code> 合并成<b>一次</b>全表遍历(汇总数 + 首个错误一次拿到);工具栏徽章改为保存后刷新,或退而求其次只统计可见行 + 防抖。</p>
<p><b>收益</b>:失焦 286→~90ms@1000、465→~90ms@5000;保存 318/612ms 同步下降。<b>风险</b>:无(徽标语义不变)。</p>
</div>
<div class="fix p0">
<h4><span class="tag high">P0 · 组件库</span> 阻断首次点击的「列重建级联」</h4>
<p><b>位置</b>:<code>G3Table.vue:923</code>(displayColumns 依赖 <code>editingActive</code>)。</p>
<p><b>顺序</b>(从易到难,每步可独立上线):<br />
1. 先用 <code>Object.is</code> 逐层比对确认到底哪一环翻转了列引用(防止改错地方);<br />
2. <b>两态记忆化</b>:按 <code>editingActive</code> 的真/假各缓存一份列数组,行与行之间切换不重建(现已在行间切换不翻转,缺的只是首击那次);<br />
3. 最终让 customCell 注入<b>不再依赖 <code>editingActive</code></b>:由 cell 内部 <code>ctx.isEditing(row)</code> 分发(函数本就接收 row),列引用从此稳定,点击不再触碰底座几何链。</p>
<p><b>收益</b>:首次点击 298~394ms → 目标 ~50ms 以内。<b>风险</b>:中(需回归冻结列/列宽/编辑激活的 e2e)。</p>
</div>
<div class="fix">
<h4><span class="tag mid">P1 · 根子 · 工作量最大</span> 虚拟滚动行级化,消灭每帧全表重渲染</h4>
<p><b>位置</b>:<code>stk-table-vue</code>(上游库)或 G3Table 的 tbody 组织方式。</p>
<p><b>做法</b>:把 tbody 行拆成独立子组件(每行一个渲染作用域),行数据不变的行跳过 patch;或对可见窗口做更细粒度的 diff 复用。这是 RC1 的正解,也是让纵向从 15~18 FPS 回到 60 FPS 量级的唯一路径。</p>
<p><b>收益</b>:滚动卡顿整体解决(横向同步受益于 RC6 修复)。<b>风险</b>:高(触及底座渲染核心,需完整 e2e 回归:虚拟滚动、冻结列、合并单元格、树形、区域选择)。</p>
</div>
<div class="fix">
<h4><span class="tag mid">P1 · 组件库</span> updateSelectionDOM 不再随滚动窗口全量跑</h4>
<p><b>位置</b>:<code>useAreaSelection.ts:319-342</code>。</p>
<p><b>做法</b>:watcher 去掉对 <code>startIndex/endIndex</code> 的依赖(滚动换窗时按需增量更新选区属性即可);选区为空时早退;把「逐格生成 cellKey 查 Set」换成按 <code>selectedCellKeys</code> 直接定位 <code>td[data-col-key]</code> 批量 <code>setAttribute</code>。</p>
<p><b>收益</b>:点击后滚动 8~11 FPS → 15~18 FPS 档。<b>风险</b>:低-中(需回归拖选/Shift 扩选/复制)。</p>
</div>
<div class="fix">
<h4><span class="tag mid">P1 · 组件库</span> 校验态按值缓存 + 消灭每格一个 computed</h4>
<p><b>位置</b>:<code>utils/editable.ts:369-454</code>(displayValue)、<span class="file">_composables/useNamespace.ts:41</span>。</p>
<p><b>做法</b>:①校验结果按 <code>(dataIndex, value)</code> 做值键缓存,注意<b>跨列校验</b>(如「目的港≠起运港」)的失效条件——按列声明依赖或按 (row 版本号 + value) 失效;②<code>useNamespace</code> 的 <code>b/e/is</code> 是纯字符串拼接,下沉为模块级单例,别在每格渲染里新建 computed。</p>
<p><b>收益</b>:滚动每帧几百次 validate/checkPrecision 大幅收敛(参照 A/B:去 editor 后纵向 +38% 中的一部分)。<b>风险</b>:中(缓存失效条件是主要出错点,需单测覆盖跨列校验)。</p>
</div>
<div class="fix">
<h4><span class="tag mid">P1 · 组件库</span> 行内编辑从「整行挂载」改为「只挂当前格」</h4>
<p><b>位置</b>:<code>utils/column.ts:121-134</code> + <span class="file">utils/editable.ts:1177</span>。</p>
<p><b>做法</b>:date 已是「只给 focusCol 挂编辑器」的模式,把该模式推广到 text/number/select;非当前格走 displayValue(一个 span)。</p>
<p><b>收益</b>:换行点击 94~250ms → 接近同行换列的 0.7ms 量级;编辑态滚动每帧少几百个函数式组件。<b>风险</b>:中(键盘连续录入 / Tab 跳格 / Enter 下移链路需 e2e 兜底)。</p>
</div>
<div class="fix">
<h4><span class="tag mid">P1 · 组件库</span> 横滚干净 A/B:定位 RC6 与 sticky 的真实占比</h4>
<p><b>位置</b>:<code>style/_overrides.scss:460</code>(恒常 sticky 补丁)+ <span class="file">useFixedCol.ts:157-164</span>。</p>
<p><b>做法</b>:A/B 只切 <code>position</code>、保留 z-index 与观感(避免「一起改混味道」);同时验证 RC6 机制——固定列 class map 无变化时跳过写入(先算字符串、有变化再赋值)。两个假设不互斥,用同一组实验一起验。</p>
<p><b>收益</b>:横向 20~26 FPS → 60 FPS 量级;顺带消除 100 行区间的横滚塌陷。<b>风险</b>:低(观感回归为主)。</p>
</div>
<div class="fix p2">
<h4><span class="tag low">P2 · 顺手</span> will-change 限定在 transform 滚动模式</h4>
<p><b>位置</b>:<code>G3Table.vue:1932-1941</code>。无条件给约 28000px 高的 tbody 挂合成层;库自己仅在 <code>.exp-scroll-y</code> 下才加(<span class="file">stk-table-vue style.less:392-396</span>)。与该条件对齐即可。实测单独去除改善有限(41→38ms),属低成本顺手项。</p>
</div>
<div class="fix p2">
<h4><span class="tag low">P2 · 记录</span> 100 行区间记为已知边界</h4>
<p><code>MIN_VIRTUAL_ROW_COUNT = 100</code> 的门控是合理设计(小表示全量渲染更稳),<b>不建议为性能调高</b>——那只是把问题藏进「DOM 全量」。待 P1 修复落地后,该区间的横滚塌陷会随 RC6 一起消失,届时可重新评估阈值。</p>
</div>
</section>
<section>
<h2>附录 A · 关键代码位置索引</h2>
<table>
<tr><th>文件</th><th>行</th><th>内容</th></tr>
<tr><td><code>g3soft-libs/packages/ui/src/table/G3Table.vue</code></td><td class="num">251</td><td>纵向虚拟门控 <code>MIN_VIRTUAL_ROW_COUNT = 100</code></td></tr>
<tr><td></td><td class="num">584</td><td><code>activateEditing</code> / <code>editingActive</code> 翻转</td></tr>
<tr><td></td><td class="num">923</td><td><code>displayColumns</code> 全量重建(RC2 入口)</td></tr>
<tr><td></td><td class="num">1207</td><td><code>handleAreaSelectionChange</code>(点击 → 进编辑链路)</td></tr>
<tr><td></td><td class="num">1932</td><td>tbody 无条件 <code>will-change: transform</code></td></tr>
<tr><td><code>g3soft-libs/packages/ui/src/table/utils/column.ts</code></td><td class="num">121</td><td>customCell 注入判据(editingActive 后 10 → 17 列)</td></tr>
<tr><td><code>g3soft-libs/packages/ui/src/table/utils/editable.ts</code></td><td class="num">369</td><td><code>displayValue</code>:展示态每格跑校验(RC5)</td></tr>
<tr><td></td><td class="num">1177</td><td><code>createEditableCell</code>:行级编辑器分发(RC3)</td></tr>
<tr><td><code>g3soft-libs/packages/ui/src/_composables/useNamespace.ts</code></td><td class="num">41</td><td>每格渲染新建一个 computed(RC5)</td></tr>
<tr><td><code>g3soft-libs/packages/ui/src/table/style/_overrides.scss</code></td><td class="num">460</td><td>fixed-cell 恒常 sticky 补丁(A/B 对象)</td></tr>
<tr><td><code>stk-table-vue/src/StkTable/StkTable.vue</code></td><td class="num">1240</td><td>columns 变化 → 全量重算链(RC2)</td></tr>
<tr><td></td><td class="num">1927</td><td><code>onTableScroll</code>:rAF 节流的滚动入口(RC1)</td></tr>
<tr><td></td><td class="num">1945</td><td>横滚分支调用 <code>updateFixedShadow</code>(RC6)</td></tr>
<tr><td><code>stk-table-vue/src/StkTable/useFixedCol.ts</code></td><td class="num">157</td><td>每次横滚无条件写新数组 → class map 失效(RC6)</td></tr>
<tr><td><code>stk-table-vue/src/StkTable/features/useAreaSelection.ts</code></td><td class="num">319</td><td>选区 watcher 依赖 startIndex/endIndex(RC7b)</td></tr>
<tr><td></td><td class="num">217</td><td><code>updateSelectionDOM</code> 全可见格遍历(RC7b)</td></tr>
<tr><td><code>web/src/views/table-test/index.vue</code></td><td class="num">615</td><td><code>errorCount</code> computed(RC4)</td></tr>
</table>
</section>
<section>
<h2>附录 B · 探针资产与复跑</h2>
<p>两份排查的探针脚本均为只读(不改业务代码、不请求业务接口、可随时删):</p>
<ul>
<li>真实页探针(10 个):<code>g3soft-libs/packages/ui/probe-table-*.mjs</code>,如 <code>perf</code>(基线)、<code>gpu</code>(三路径对照)、<code>churn</code>(DOM 变更计数)、<code>final</code>(克隆 + A/B)、<code>ab</code>(观感 A/B)。用法示例:<code>node probe-table-perf.mjs --gpu=none --case=vertical-1000 --seconds=5 --rows=5 --repeat=3 --json</code>。运行前置:前端 5090 与后端 8088 已起。</li>
<li>复刻页探针(已删除):曾用 <code>web/__perf-table.html</code> + 系列脚本做 virtual 开/关与 errorCount 开关对比,结论已并入本报告;如需复现按附录 A 的列定义重建即可。</li>
</ul>
<div class="note">
<b>权限说明</b>:<code>demo_zhangwei</code> 访问 <code>/table-test</code> 会 403(该账号只授了 <code>menu.approval%</code> 与 <code>action.v_b_othercompany.%</code>)。<code>sql/20_registration/13_table_test_menu.sql</code> 只建菜单、<b>不建权限点</b>——要普通账号可进,需执行其末尾「可选:给普通账号授权」段;或直接用 <code>g3soft</code>。
</div>
</section>
<section>
<h2>附录 C · 口径与已知局限</h2>
<ul class="no">
<li>帧率均为<b>相对度量</b>(同一台机、同一口径下的对比),绝对值随机器浮动;结论的有效性来自对照实验(克隆 / 开关 / A/B),不是绝对数值。</li>
<li>数据取自单机单次测量(2026-10-16);关键结论均有两个以上独立实验互证。</li>
<li>生产构建(<code>pnpm build</code> 产物)下的表现未验证;dev 模式有额外开销,但不影响根因归属。</li>
<li>「横滚 sticky 假设」与「RC6 class map 机制」尚未用干净 A/B 分离占比,已列入 P1 实验项。</li>
</ul>
</section>
<footer>终版合并自两份独立排查(2026-10-16);所有结论基于实测对照,未改动任何业务代码。</footer>
</div>
</body>
</html>