2025年4月12日,Claude Code 仓库里出现了一个 issue:#769,标题是「进行中的调用导致屏幕闪烁」。它到现在还开着。Peter Steinberger 把它叫作 Claude Code 的标志性闪烁——工具输出流进来的时候,整个屏幕跟着眨。

打开网易新闻 查看精彩图片

2025年12月17日,Anthropic 的 Thariq Shihipar 发帖说:「我们重写了 Claude Code 的终端渲染系统,把闪烁减少了大约 85%」。同一段文字被 Boris Cherny 转发。这个修复后来被回滚了。2026年1月21日,他再次发帖说已经向所有人重新推送,并附上了流水线说明。其中一个数字值得记住:约 16 毫秒的帧预算,其中大约 5 毫秒用于从 React 场景图走到写出 ANSI。

85% 是团队自己的数字,而 85% 不是 100%。剩下的那部分,取决于你的终端知不知道一帧在哪里结束。

终端不知道你的「帧」在哪结束

终端是一格一格的网格。Claude Code 这样的程序不画像素,它写字节:可打印的文本,加上移动光标、擦除行、改变颜色的转义序列。终端把这串流解析进自己的网格,再按自己的节奏把网格画到屏幕上。

问题就在这里。如果应用把光标上移、擦掉十行、再分几次写入十行新内容,终端完全可以在其中任何一次写入之后就开始画。在擦除之后抓到它,你看到一块空白;在半途抓到它,你看到旧帧的一半压着新帧的一半。按常见的 60 Hz 刷新率,这就是闪烁。

Chris Lloyd 在 Hacker News 上谈「我们的流水线」时说,大多数人把 Claude Code 想成「就是个 TUI」,但它更接近「一个小型游戏引擎」。每一帧,流水线用 React 构建场景图、做布局、光栅化到一块 2D 屏幕、和上一帧做差分,最后才把差分变成 ANSI 序列。在单元格层面做差分意味着每帧字节更少,被撞上半途更新的机会也更少,但任何一帧只要还需要不止一次写入,就可能被画成半成品。

?2026h 和 ?2026l 到底做什么

「终端画得太早」的解法,是告诉终端一帧什么时候开始、什么时候结束。这就是 DEC 私有模式 2026,同步输出,由 Christian Parpart 在 Contour 终端的规范里写下。

  • ESC [ ? 2026 h(设置模式)开始一次同步更新。终端继续读取和处理你发来的一切,但屏幕上保持它最后画出的那一帧。
  • ESC [ ? 2026 l(重置模式)结束它。终端把网格的当前状态一次性画出来。

这就是双缓冲,由应用来决定何时交换。规范承认,终端在放弃之前应该等结束标记多久,目前没有共识;它也指出,超时设得太短,并不比完全不做同步更糟。

你可以用 DECRQM 问终端它认不认识这个模式。在 bash 里:

printf '\e[?2026$p'
IFS= read -rs -t 1 -d y reply
printf '%q\n' "$reply"

看回复里分号后面的那个数字。按规范,1 或 2 表示支持(当前已设置或已重置),0 表示不认识,4 表示永久重置,同样等于不支持。一秒内没有回复,也算不支持。Claude Code 自己会做这个探测:文档说它在启动时检查同步输出支持,终端报告支持时才会使用。

tmux 为什么还在闪:issue #37283

2026年3月22日,一位用户提交了 #37283:在 WSL2 和 Windows Terminal 下,tmux 3.4 分屏里跑 Claude Code 2.1.81,只要输出在流式写入就会闪,光标每次更新都跳到渲染区顶部再跳回来。空闲的面板则纹丝不动。报告者把它追到重绘没有被包在 DECSET 2026 里,并提出了显而易见的修法:在光标移动和清屏之前发 \e[?2026h,之后发 \e[?2026l。

难点在于,tmux 自己也是一个终端。它把应用的输出解析进每个面板各自的网格,再把那个网格重画到你的真实终端上。所以应用的标记必须被 tmux 理解,而不是被转发。tmux 的 terminal-features 里的 sync 值管的是另一跳,即 tmux 把自己的重绘同步到外层终端。4月14日,一位评论者在 issue 下指出,当时还没有任何已发布的 tmux 支持应用这一侧。

tmux 这一侧其实早就写好了。Chris Lloyd 在2025年12月5日提交了 tmux PR #4744:当面板里的应用发出 ESC[?2026h,tmux 就暂缓该面板的输出,直到 ESC[?2026l 或 1 秒超时,并在尺寸变化时丢弃该模式,好让 tmux 总能重绘。维护者把它合进了 OpenBSD 树,tmux 的 CHANGES 文件把它列在 3.7 之下,同一条目里还有「响应 DECRQM 2026」。

同一位作者还为 xterm.js 写了 PR #5453,那是 VS Code 内置的终端组件。模式 2026 的请求,即 xterm.js issue #3375,在2025年12月20日被一个题为「Add synchronized output support (DEC mode 2026)」的提交关闭。

#37283 本身在5月28日因过期被机器人以「不计划处理」关闭。Claude Code 的文档现在把规则写得很直白:tmux 3.6 系列及更早的版本没有实现同步输出,在它们下面跑,你看到的闪烁可能比直接在终端里跑 Claude Code 更多。升级到 tmux 3.7 或更高,或者把 Claude Code 放在单独的标签页里跑。用 tmux -V 查你的版本,最新发布是 3.8。

在自己的终端里看一眼

有一段脚本会把一个 20 行的方框重画 150 次。每一帧先清屏,等 10 毫秒模拟缓慢的布局,然后一行一行写入,每行之后都 flush 一次。传入 sync 参数,它就把每一帧包进模式 2026 的那对标记里。

把它存成 flicker.py,用一个至少 22 行高的窗口。先跑 python3 flicker.py,再跑 python3 flicker.py sync,然后打开 tmux 把两个都再跑一遍。

该看什么,按规范而不是按我的屏幕:没有 sync 时,那 10 毫秒的空白夹在两次写入之间,终端可以把它画出来,方框就会眨。在支持该模式的终端上开着 sync,终端会一直显示上一帧完整画面直到结束标记,所以应该只有帧计数器在变。在不认识该模式的终端或 tmux 上,两次运行看起来一样,这正是 #37283 描述的情形。finally 那一行保证即使你在帧中途按 Ctrl+C 也会发出结束标记,不会有东西卡住。

顺带说一句,那个 16 毫秒的帧预算,和「结束标记没来就 1 秒超时放弃、而不是冻住面板」的取舍,跟你在任何后端里做的延迟与超时判断是同一类决定。