313 lines
28 KiB
HTML
313 lines
28 KiB
HTML
<!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>:数据量 > 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>
|