perf(dashboard): 会话四视图去掉整树重渲染,并修 listen 探测的 close 死锁 - #1378
Open
deepcoldy wants to merge 1 commit into
Open
Conversation
会话控制页在 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
改了什么
会话控制页(Dashboard → 会话控制)在 5800 会话的真实数据下,点任意一个按钮都要卡好几秒。
根因有三处,形状一致:只改了一点状态,却重建/重算了整棵树。
1. 视图切换靠换 key 重放入场动画 ⟹ 每次切视图都把整个视图从零重建
原来入场动画是
key={viewStageAnimKey}驱动的——换 key 等于告诉 React「这是另一个组件」,于是点一次「看板/状态板/话题/表格」,当前视图连同全部卡片全部卸载再挂载。
改成就地重启 CSS 动画:摘掉 class → 读一次
offsetWidth触发 reflow → 加回去。DOM 不动,React 不重建子树。四个视图同时从
hidden={...}改为按需挂载,非当前视图不再留在树里(切完一轮后 DOM 节点从 460099 → 11546)。2. 表格 / 状态板 / 话题三个视图完全没有 memo,回调全是 JSX 内联箭头
于是「打开/关闭详情」这种只改抽屉状态的操作,也要把上千行 DOM 重排一遍。
三个视图统一 memo 化,回调收进
useCallback固定引用。3. 看板卡片:选中一张卡片会让所有卡片重算
卡片是唯一按会话数线性增长的东西(bot 分组下 1406 张)。按「这张卡片会不会长得不一样」写了
等价函数(
dragId/dropBeforeId/editingId折成布尔、callbacks逐字段比),并把 icons 对象提到模块级、
cardProps收进useMemo。顺带:修掉 listen-with-probe 的一个静默死锁
verifyBound拒绝某端口后走releaseAndStep,而server.close()只停止 accept、会等已接受的连接排空,偏偏步进与日志都在它的回调里。于是一个赖着不走的连接就能让整个探测无声卡死——
没有 LISTEN、不步进到下一个端口、一行日志都没有。加
closeAllConnections()先拆连接。为什么
用户反馈「看板」卡,修完看板后又反馈「状态板、话题、表格中还是有些卡,主要体现在点击某些按钮时,例如『详情』的打开和关闭」。
这三处根因不同但形状相同,一起修。
效果(真机 5800 会话,Playwright + PerformanceObserver,取三次中位数)
详情抽屉开/关 — 这是用户直接反馈的场景:
视图切换 / 看板分组切换:
影响面
sessions-page.tsx/sessions-kanban.tsx)与utils/listen-with-probe.tslisten-with-probe被 dashboard 与 dashboard-ipc 共用,已跑两边相关测试验证
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(话题视图开详情):
showModal706ms(19.5%) — 浏览器原生 top-layer 提升getBoundingClientRect490ms(13.5%) — 来自floating-scrollbars.ts的readTargets()全树扫描:话题视图 119502 个节点扫 355ms,只为找出 5 个可滚动元素
这两项要么需要虚拟化、要么需要把观察范围收窄到子树,属于另一个量级的改动,本 PR 刻意不碰。
界面截图
🤖 Generated with Claude Code