Skip to content

perf(dashboard): 会话四视图去掉整树重渲染,并修 listen 探测的 close 死锁 - #1378

Open
deepcoldy wants to merge 1 commit into
masterfrom
wt/dashboard-2
Open

perf(dashboard): 会话四视图去掉整树重渲染,并修 listen 探测的 close 死锁#1378
deepcoldy wants to merge 1 commit into
masterfrom
wt/dashboard-2

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

改了什么

会话控制页(Dashboard → 会话控制)在 5800 会话的真实数据下,点任意一个按钮都要卡好几秒。
根因有三处,形状一致:只改了一点状态,却重建/重算了整棵树

1. 视图切换靠换 key 重放入场动画 ⟹ 每次切视图都把整个视图从零重建

原来入场动画是 key={viewStageAnimKey} 驱动的——换 key 等于告诉 React「这是另一个组件」,
于是点一次「看板/状态板/话题/表格」,当前视图连同全部卡片全部卸载再挂载。

改成就地重启 CSS 动画:摘掉 class → 读一次 offsetWidth 触发 reflow → 加回去。DOM 不动,React 不重建子树。

这个做法成立有个前提,代码里也写了注释:JSX 上的 className 必须是静态字符串
React 只在 className 的计算值变化时才写 DOM,值恒为 "sessions-view-stage"
所以它不会把我们命令式加上去的 class 冲掉,也就不需要再拿一个 state 去驱动它。

四个视图同时从 hidden={...} 改为按需挂载,非当前视图不再留在树里(切完一轮后 DOM 节点从 460099 → 11546)。

2. 表格 / 状态板 / 话题三个视图完全没有 memo,回调全是 JSX 内联箭头

于是「打开/关闭详情」这种只改抽屉状态的操作,也要把上千行 DOM 重排一遍。
三个视图统一 memo 化,回调收进 useCallback 固定引用。

⚠️ memo 会挡住父组件触发的重渲染,而这三个视图内部直接调 t()
所以每个都补了 useT() 自己订阅 locale——否则切语言时文案会停在旧语言上。
这条做了反变异验证,见下。

3. 看板卡片:选中一张卡片会让所有卡片重算

卡片是唯一按会话数线性增长的东西(bot 分组下 1406 张)。按「这张卡片会不会长得不一样」写了
等价函数(dragId/dropBeforeId/editingId 折成布尔、callbacks 逐字段比),
并把 icons 对象提到模块级、cardProps 收进 useMemo

顺带:修掉 listen-with-probe 的一个静默死锁

verifyBound 拒绝某端口后走 releaseAndStep,而 server.close() 只停止 accept、会等已接受的连接排空
偏偏步进与日志都在它的回调里。于是一个赖着不走的连接就能让整个探测无声卡死——
没有 LISTEN、不步进到下一个端口、一行日志都没有。加 closeAllConnections() 先拆连接。

为什么

用户反馈「看板」卡,修完看板后又反馈「状态板、话题、表格中还是有些卡,主要体现在点击某些按钮时,例如『详情』的打开和关闭」。
这三处根因不同但形状相同,一起修。

效果(真机 5800 会话,Playwright + PerformanceObserver,取三次中位数)

详情抽屉开/关 — 这是用户直接反馈的场景:

视图 开详情 关详情
状态板 1408ms → 736ms 1588ms → 703ms
话题 1769ms → 967ms 2071ms → 954ms
表格 556ms → 382ms 578ms → 376ms

视图切换 / 看板分组切换

操作
→ 状态板 4139ms 1583ms
→ 话题 4437ms 2050ms
→ 表格 4102ms 1111ms
→ 看板 6185ms 460ms
分组 → bot 6261ms 688ms
分组 → flow 6077ms 413ms
分组 → team 5668ms 143ms

影响面

  • 改动集中在 dashboard 前端 sessions 页(sessions-page.tsx / sessions-kanban.tsx)与 utils/listen-with-probe.ts
  • listen-with-probe 被 dashboard 与 dashboard-ipc 共用,已跑两边相关测试
  • 未触及 daemon、worker、CLI 适配器、后端(PtyBackend/TmuxBackend)、IM 层——不涉及跨 CLI / 跨后端 / 跨会话类型的共用路径
  • 纯前端渲染改动,不改任何接口与数据形状

验证

bun run build                 # tsc + esbuild 干净,0 错误
相关 8 个测试文件              # 84 passed

listen-with-probe 死锁 A/B(node 22 与 bun 各自实测):修复前 close() 回调 10s 仍未触发;修复后 0~1ms。
新增回归测试「steps up even when a client is still parked on the rejected port」,
变异源码即精准转红Test timed out in 4000ms)。

memo 引入的 locale 风险做了反变异:单独删掉话题视图的 useT() 后,切语言时话题视图停在旧语言("Details"),
而状态板/表格正常切到「详情」;恢复后三个视图全部跟随。证明这三个 useT()承重的,不是冗余。

表格列显示/隐藏功能回归:13 → 12 → 13 列,持久化 ["cliId"][] 正确;两列隐藏后重置也正确。

入场动画仍然会重放:挂 animationstart 监听,每次切视图计数都 +1,看板分组切换 6 → 7。

本次处理的残留开销(已测量,留给后续)

改完后剩下的耗时基本不在 React 层了。CDP profile(话题视图开详情):

  • React 自身工作 ≈ 1ms
  • showModal 706ms(19.5%) — 浏览器原生 top-layer 提升
  • getBoundingClientRect 490ms(13.5%) — 来自 floating-scrollbars.tsreadTargets()
    全树扫描:话题视图 119502 个节点扫 355ms,只为找出 5 个可滚动元素

这两项要么需要虚拟化、要么需要把观察范围收窄到子树,属于另一个量级的改动,本 PR 刻意不碰。

界面截图

截图使用合成会话数据渲染(bot 名、群名、工作目录、各类 ID 全部为构造值),非真实数据。

状态板 状态板 · 详情
状态板 状态板详情
话题 话题 · 详情
话题 话题详情
表格 表格 · 详情
表格 表格详情

🤖 Generated with Claude Code

会话控制页在 5000+ 会话下,点任意一个按钮都要卡好几秒。根因有三处,都是
「只改了一点状态,却重建/重算整棵树」:

1. 视图切换靠 `key={viewStageAnimKey}` 换 key 来重放入场动画,等于每次点
   「看板/状态板/话题/表格」都把当前视图连同全部卡片从零重建。改成就地重启
   CSS 动画(摘 class → 读 offsetWidth → 加回),DOM 不动。
   四个视图同时也从 `hidden` 改为按需挂载,非当前视图不再留在树里。

2. 表格/状态板/话题三个视图完全没有 memo,且每个回调都是 JSX 上的内联箭头。
   于是「打开/关闭详情」这种只改抽屉状态的操作,也要重排上千行 DOM。三个视图
   统一 memo 化,回调全部收进 useCallback 固定引用。
   注意 memo 会挡住父组件触发的重渲染,而这三个视图内部直接调 t(),所以每个
   都补了 useT() 自己订阅 locale——否则切语言时文案会停在旧语言上。

3. 看板卡片同理:卡片是唯一按会话数线性增长的东西(bot 分组下 1406 张),
   选中一张卡片却会让所有卡片重算一遍。按「这张卡片会不会长得不一样」写了
   等价函数,并把 icons 对象提到模块级、cardProps 收进 useMemo。

顺带修掉 listen-with-probe 的一个静默死锁:verifyBound 拒绝某端口后走
releaseAndStep,而 server.close() 只停止 accept、会等已接受的连接排空,
步进与日志又都在它的回调里。于是一个赖着不走的连接就能让整个探测无声卡死——
没有 LISTEN、不步进到下一个端口、一行日志都没有。这正是 dashboard 前几天
起不来的原因。加 closeAllConnections() 先拆连接。

影响面:改动集中在 dashboard 前端 sessions 页与 listen-with-probe。
listen-with-probe 被 dashboard / dashboard-ipc 共用,已跑相关测试;
未触及 daemon、worker、适配器、IM 层。

验证:
- bun run build 干净;相关 8 个测试文件 84 passed
- listen-with-probe 死锁 A/B(node 22 与 bun 各自实测):
  修复前 close() 回调 10s 仍未触发,修复后 0~1ms;新增回归测试,
  变异源码即精准转红(Test timed out in 4000ms)
- memo 引入的 locale 风险做了反变异:删掉话题视图的 useT() 后,切语言时
  话题停在旧语言而另外两个视图正常;恢复后三个视图全部跟随
- 真机(5800 会话)前后对比见 PR 描述

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant