Qwen Code 改进建议
April 5, 2026 · View on GitHub
核心洞察:CLI Agent 的生命周期往往极长。在一个重度开发的下午,终端里可能积累了超过 1000 轮的对话记录(包含冗长的 Diff、大模型的思维链、复杂的表格)。在默认的 React Ink 或其他终端 UI 引擎下,即使这些内容已经被卷动到了屏幕不可见的上方,渲染引擎依然会在内存里维护它们的 DOM 树并参与每一次重绘的布局计算(Layout Calculation)。这会导致终端在高频交互时出现严重的 CPU 飙升与输入延迟。Claude Code 采用了前端页面中常用于海量数据的“虚拟列表(Virtual List)”技术,做到了在无限长的会话中依然保持 O(1) 的渲染复杂度;而 Qwen Code 目前依然是全量渲染。
返回 改进建议总览
一、长会话导致的终端性能衰减
1. Qwen Code 的现状:全量 DOM 树与 Layout
当你在 Qwen Code 里积累了超过几百条记录后:
- 痛点一(渲染极其缓慢):每次你在底部的输入框按下任意一个字母(触发状态更新),或者 TUI 的 Spinner 跳动了一帧,React Ink 会遍历整个包含上千个
Message组件的树。 - 痛点二(内存与崩溃风险):由于 TUI 引擎需要计算每一行文字是否要换行(Word Wrap),以便确定屏幕整体高度。这几千个节点的测宽计算和字符串拼接会吃满单线程的 CPU,并导致终端模拟器(如 iTerm2 或 Alacritty)在面对瞬间推送的几十兆 ANSI 字符时直接卡死或崩溃。
2. Claude Code 解决方案:视口级渲染截断
在 Claude Code 的 hooks/useVirtualScroll.ts 和 components/VirtualMessageList.tsx 中,他们手工实现了一套针对终端的极客级虚拟滚动。
机制一:高度预计算与缓存 (Height Memoization)
虚拟滚动的核心难点在于:终端的每一个 Message 高度是不等的(有的只有一行字,有的是 50 行代码)。
Claude Code 会在每个消息组件首次渲染时,将它占据的终端行数(Rows)记录在一张全局的高度字典中。如果消息内容没变且终端宽度没变,它永远不会再去测算这个节点。
机制二:基于 Viewport 的窗口裁剪
引擎通过监听 process.stdout.rows 获取当前可见的物理屏幕高度(如 40 行)。
它只会把处于 [当前卷轴位置 - 缓冲高度, 当前卷轴位置 + 物理高度 + 缓冲高度] 区间内的消息节点喂给真正的 React Ink 渲染层。
对于那些早就滚到屏幕上方 500 行开外的旧历史,引擎直接用空白的空组件替换它们(只保留它们的占位高度),绝对不输出任何实际字符。
机制三:按需平滑滚动机制
为了配合原生终端的滚轮行为,Claude 甚至劫持了鼠标滚动事件或方向键。当用户向上翻页时,底层的游标改变,虚拟列表瞬间释放旧节点并加载新节点,其速度快到人眼根本无法察觉这是被“按需加载”出来的。
二、Qwen Code 的改进路径 (P3 优先级)
对于想做成“程序员一整天都不用关闭的超级副驾驶”而言,解决历史堆积是必答题。
阶段 1:剥离历史静态渲染
- 修改 Qwen Code 中的
MessageList.tsx。 - 对历史数组(不包含正在打字的 current turn)进行切分:最近的 50 条消息作为“热数据”保留渲染能力。
- 更早期的消息,可以直接在内存中转化为纯文本(剥离复杂的 React 组件树开销),并丢给原生的
console.log冲入终端历史缓冲区。
阶段 2:开发 VirtualList 容器组件
如果不采用简单的剥离,希望保留上下文随时修改的能力:
- 引入或参考
ink-virtual-list的实现原理。 - 拦截并维护所有子节点的高度总和,动态计算出当前的
visibleStartIndex和visibleEndIndex。
阶段 3:配合 Context Collapse 降维
将我们在 上下文折叠 中提到的折叠器结合起来。那些被折叠的“探索节点”高度直接变为固定的 1 行,极大地降低了虚拟滚动计算高度的复杂度。
三、改进收益评估
- 实现成本:高。终端 UI 的虚拟滚动由于涉及到繁杂的测宽测高,极易引发跳动(Jitter)和滚动条错乱,属于硬骨头。
- 直接收益:
- 无尽模式的底气:即便开了 1000 轮交互对话,终端打字和 Spinner 依然如刚启动时一般丝滑流畅,彻底释放了长上下文模型的威力。
- 大幅降低机器功耗:杜绝了每一次按键都要导致 CPU 飙升的问题,拯救笔记本用户的电池续航。