长会话里打字越来越卡?Hermes 桌面端把每次按键的开销砍到了三分之一

下午三点,你把一个跑了两天的会话往上翻,想找早上那几条结论。翻到顶之后,你回到输入框,开始打字——然后你停了一下:字是一个一个蹦出来的,删掉一个字符要等半秒,光标跟不上手指。你换了台机器,还是卡;你以为是模型在后台跑,可模型明明在等你说完话。
真正的问题在输入框本身。Hermes 桌面端 10 月 26 日合入的一项性能改动(PR #144903)把这件事说清楚了:历史加载得越多,打字越慢,因为每一次按键都会触发整段对话的重新渲染。修完之后,同一个 300 轮会话里,每次按键的主线程开销从 33.9 毫秒降到 9.6 毫秒。
一次按键,为什么会让整段对话重算一遍
Hermes 桌面端的聊天界面建在 assistant-ui 这个组件库上。输入框里的文字不是老老实实待在自己的小盒子里——它是 store(共享状态仓库)里的一个值,而界面里很多地方会订阅它:
- 发送按钮该亮还是该灰;
- 输入框里只有问号时要不要弹帮助提示;
- 这行字是斜杠命令(
/model之类)还是普通消息; - 输入框该显示占位符还是真实内容;
- 多行文本的换行布局。
问题在于,这些订阅方的检查不是“只检查输入框”,而是跟着 store 的写入一起,把已经挂在页面上的每一条消息的选择器都重新过一遍。会话越长、挂载的轮次越多,这一遍就越贵。PR 里的注释写得很直白:短会话里每次按键约 0.5 毫秒,长会话里能到 19 毫秒,而这只是其中一环。
于是就有了那个很反直觉的体验:不是越用越慢,是同一个会话里你往回翻得越多,打字越慢。因为翻回去意味着更多历史被真正挂载到 DOM 上。
改法:把“每次按键都写”换成“只在必要的时候写”
修法不是给输入框加个节流阀就完事,而是把“什么时候必须写进 store”这个判断做成了显式逻辑。新增的 composer-store-writer.ts 里有一个函数叫 composerTextEdges(text),它把上面那五件订阅方真正关心的事压成一个签名:
text.length === 0 // 空 / 非空
trimmed.length > 0 // 有实际内容
text === '?' // 单独一个问号
trimmed.length > 0 && !SLASH_COMMAND_RE // 普通消息(非斜杠命令)
text.trimEnd().includes('\n') // 多行
只要这次按键没有让这个签名发生变化,订阅方看到的仍是同一个状态,那么这次写入就可以等。等多久?代码里定了一个常量 TYPING_STORE_IDLE_MS = 180——打字停顿 180 毫秒后,尾部的文字才真正落进 store(草稿持久化和运行时镜像从那里读)。
剩下的路径也被补齐了,否则延迟写会引入新的 bug:
flush():在任何需要 store 是“当前值”的读取之前,立刻把待写文字落盘;cancel():组件卸载时丢掉待写文字,不留脏数据;observed():store 被外部(运行时订阅)改动时,把它认作新的基线,避免旧值覆盖新值。
另外那个“粘住的提示条”(sticky prompt,长会话里贴在顶部的输入回显)也不再每帧重新给每条消息算样式,改成注册一次 CSS 变量,滚动时不再重排。
实测数字
PR 里给的是一组可复现的测量:300 轮 / 1800 条消息的会话,先用 2500 帧滚轮把整段历史读一遍(所有轮次挂载,约 4.2 万个 DOM 节点),然后敲 60 个字符。
| 指标 | 改动前 | 改动后 |
|---|---|---|
| 每次按键的主线程耗时(历史已读满) | 33.9 ms | 9.6 ms |
| 每次按键的主线程耗时(刚打开会话) | 12.1 ms | 6.0 ms |
| 每次按键时 store 刷新的耗时 | 18.0 ms | 1.3 ms |
作者也把边界写清楚了,这点值得照抄:这些是共享机器上的单次测量,另一次“改动前”的对照跑出了每次按键 49.3 毫秒;在这台 20 核机器上,从按下到画面更新本来就在一帧左右,所以 33.9 和 9.6 的差别主要落在慢机器上——主线程越紧张,输入延迟越明显。换句话说,这不是一个“所有人体感都翻三倍”的改动,而是“你的笔记本不用再替历史付打字税”。
你该做什么
这次改动目前只在 main 分支上:v0.22.5 是 10 月 23 日发布的,而 PR 是 10 月 26 日合入的,所以哪怕是当天的 canary 标签也还不包含它。三条路:
- 等下一个版本发布后
hermes update升级——最省事; - 跟
main走(hermes update可以切换通道,具体见 升级通道说明); - 现在就想缓解,先把会话拆短:长会话的输入开销跟挂载的历史成正比,开新会话或做一次 会话边界整理 比什么都直接。
如果你本来就觉得“桌面端比 CLI 卡”,可以顺手看一下 启动延迟那件事——启动慢和输入慢是两个独立的问题,这次只解决了后者。
这件事的真正含义
大多数人把“agent 卡”归因到模型:token 太多、上下文太长、provider 在排队。这次的问题提醒我们,界面层也会长出自己的性能债,而且它长得特别隐蔽——它不在日志里,不在 trace 里,只在你的手指和屏幕之间。
判断方法很简单:下次觉得卡,先看卡的是“打字”还是“回复”。打字卡、回复正常,那是渲染问题;两边都卡,才去看模型和网络。把这两件事分开,排查时间能省一半。