两周把 claude.ai 提速 3 倍:和 Claude 一起做性能优化的方法
根据 Anthropic 工程师的文章整理:他们在一个 Slack 频道里和 Claude 协作,两周合并三千多个改动,把 claude.ai 的核心体验提速约 3 倍,没有出过事故。讲清“先找到能数的指标”的思路、每个线程的循环、护栏和人的分工。
本文根据公开分享整理,本站尚未实测。操作前请对照文末的原始来源。
这是“Claude Code 开发系列”的第六篇。
这一篇解决的问题是:想让 Claude 长时间、大规模地替你优化一样东西,该怎么组织这件事,才能又快又不出事。 原文是一次真实冲刺的复盘:今年 8 月,Anthropic 的一个小组用两周把 claude.ai 网页和桌面应用的核心体验提速到原来的约 3 倍,合并了三千多个改动,没有一次影响用户的事故或回滚。
先说明一点:他们用的是 Claude Tag(测试版,在 Slack 里工作的 Claude),跑的是一个内部研究模型,原文说能力大致相当于 Opus 5.5。你手上的工具不一定一样,但方法是通用的。
结果
他们盯的是占用户活动 95% 的四条路径:启动应用、开始对话、打开已有对话、发送消息。按第 75 百分位算:
| 指标 | 之前 | 之后 |
|---|---|---|
| 全新打开 claude.ai 到可以打字 | 3.1 秒 | 0.55 秒 |
| 开始一个新的 Claude Code 会话 | 0.8 秒 | 0.3 秒 |
| 加载一个 Claude Cowork 云端会话 | 2.6 秒 | 0.73 秒 |
数据来自真实用户监控,对比的是 8 月 13 日和 8 月 27 日。
起点:一个频道,一段常驻的说明
冲刺开始前,他们建了一个 Slack 频道,写了一段常驻的指示。大意(译自原文,有删节):
@Claude 你的工作是推动和 claude.ai 网站、桌面应用性能有关的一切。
职责包括:盯每次发布有没有性能退步,评估现有监测数据是否准确和全面,
维护好观测面板,对观察到的问题和容易摘的果子主动动手解决,
提出性能项目的机会,并和人类同事沟通。
这个频道的最终目标是让你尽可能自主,但我们知道今天还做不到。
接着做了三件事:
- 找出最值得优化的路径。 让 Claude 通过 Datadog 的 MCP 服务器分析使用数据,得出上面那四条路径。算上网页和桌面、不同产品,一共是 13 个测量点。
- 把基线量准。 补埋点,直到这些测量可以直接比较:每一个都从用户的操作开始,到结果渲染出来结束,并且分得清客户端和服务端各花了多少。
- 定目标。 人工挑了大约 20 个项目,每个对应一条路径。Claude 估算每个项目能省多少毫秒,加起来就是冲刺的目标。
结果第三天就达成了 13 个目标里的 12 个。于是重新定了目标,并且让 Claude 自己提新方向。原文里的那条消息(译自原文,有删节):
@Claude 原来清单上的项目我们几乎都做了,还多做了不少。来刷新一下:
还有什么没探索过,哪些可以继续爬坡,现在机会最大的在哪里?
我愿意听离谱的点子
核心经验:能数出来的,就能优化
这是全文最值得带走的一点。
他们想比发布的节奏迭代得更快。Claude 可以连续工作很多小时,甚至一整夜,不能每个原型都等线上数据。所以要找能在实验环境里测的指标。
真实耗时是用户感受到的,但它有噪声,毫秒级的数字拿来当 CI 的关卡太不稳定。工程师 Sam 问了一句:能不能不测耗时,改成数 JS 的指令数?Claude 给出了一串确定性的计数方式:
- 纯 JS 的热点路径:在 Valgrind 下用
node --predictable跑基准,直接数 CPU 指令数,和仓库里存的基线比。跑一次就行,不需要做统计。 - 浏览器里的路径:数每次交互的 React 提交次数、V8 精确覆盖率给出的函数调用次数、布局和样式重算的次数、DOM 变动的次数。
十一分钟后,五个线程同时在跑,各盯一种计数。
每个新基准都要先被怀疑。 一个基准有两份工作:先是 Claude 在实验环境里能推动的指标,然后是 CI 里一个只许降、不许升的数字。如果它不稳定,或者和用户感受到的延迟不相关,就扔掉,免得 Claude 爬错了山。他们的原话(译自原文):
@Claude 请证明对这些指标逐个爬坡,都能带来可以测到的真实耗时改善。
证明不了的候选,对应的基准我们会撤掉
验证指令数的那一次:他们让 Claude 压两条热点路径的指令数,一条是组装对话消息树的例程,一条是扫描 Claude Code 输出里状态行的扫描器。Claude 用 Valgrind 分析后发现,第一条路径有四分之一的指令花在字典查找上,同一个消息 ID 被解析了三次。
一小时后的结果:
| 路径 | 指令数减少 | 真实耗时减少 |
|---|---|---|
| 组装消息树 | 48% | 78% |
| 状态行扫描 | 31% | 44% |
指令数确实跟得上真实耗时,于是加了两个“棘轮”:之后任何让这两条路径指令数上升的 PR,CI 直接不过;每天有一个任务,在数字下降后把上限跟着调低。
原文对此的总结:以前,测量是“第零步”,加一个指标,等数据进来,然后才开始理解问题。有了 Claude,测量就是爬坡的第一步,一旦有了要打败的数字,它就能开始优化。所以人能做的杠杆最大的事,是找到更多可以测的东西。
每个线程里的循环
所有工作都在同一个频道里,按线程进行。稳定下来之后是这样一个循环:
- 有人开一个线程,说某条路径的某一段慢,通常附截图或录屏。
- Claude 追踪这段流程,找到或者新建一个能复现问题的基准。
- 在实验环境里有了像样的结果后,Claude 提交 PR。往往是好几个,按风险和评审的方便拆开,用户看得见的改动都放在开关后面。
- 上线后,Claude 盯着这次发布,读线上数据。
- 有改善,就把基准的上限调低,把成果锁住;没有,就关掉开关,再改。
- 然后在同一条路径上找下一个慢的地方。
一个例子:侧边栏的跳动
有人发了一段录屏:页面加载后,侧边栏的条目一个个蹦出来,聊天和 Cowork 的条目出现的时间不一样,页面看着发抖。现有的监控都发现不了。最接近的指标是累计布局偏移,但每次偏移只有大约 0.008 分,远低于 0.1 的“良好”线。
Issac 想到直接用底层的 Layout Instability API。Claude 做了这几件事:
- 新建一个监测事件,把每次布局偏移的来源对应到有名字的区域(侧边栏、对话记录)和阶段(首次绘制之前、可以打字之后)。
- 写一个集成测试:打开一个侧边栏有内容的页面,把侧边栏的数据扣到首次绘制之后才给,任何有名字的区域发生偏移就失败。
- 用它来证明修复有效:在主分支上跑 20 次红 20 次,在 PR 上跑 20 次绿 20 次。
事件上线后,Claude 读线上数据,发现 31% 的网页加载在页面可用之后、没有用户操作的情况下有东西在动。然后按名字一个个查原因:迟到的标题行、用户名加载后往旁边滑的光标、滚动条出现时被挤动的列表。先修掉最严重的一批,再找下一批。
这只是一个线程。冲刺期间,同时在跑的线程超过一百五十个。
铺开之后
循环在一个线程上跑通之后,多开几个就行。线程最初的需求完成后,Claude 不会停,单个线程会提出五十个、有时上百个优化 PR。越来越多的新线程是 Claude 自己开的,来自别的调查或夜间任务里的发现。
原文列的一些发现,可以看出“量什么就发现什么”:
- 数 React hook:输入框的打字路径上有 6,900 个 hook 和 900 个状态订阅,每敲一个键都重新渲染。
- 数样式重算:一个
:root:has()选择器让每次 DOM 变化多花 24 毫秒。 - 追踪首次绘制之后的代码路径:一处遗留的
location.reload(),每天造成 50 万次用户看不见的重新加载,所有加载指标都看不到它。 - 读空闲标签页的性能采样:同样的缓存快照每分钟被复制进 IndexedDB 两次,全在主线程上。
还有一个意外的:给写完的代码块做语法高亮,可能让页面卡住大约一秒。原因是破折号。回复的 Markdown 里只要有一个非 Latin-1 的字符(比如破折号、弯引号),V8 就把整个字符串存成 UTF-16,高亮用的每一条正则都走更慢的双字节路径。Claude 用二十行改动解决:高亮之前把每个代码块复制成单字节字符串。
最忙的时候一天合并两百多个改动。大约三分之一的 PR 带着新的监测或护栏,而每一个新的测量工具又带出更多的线程。
护栏:为什么三千多个改动没出事
他们动的几乎全是最热的路径(首次绘制、输入框、对话记录),所以安全机制是事先定好的:
- 每个 PR 都过自动评审,并且至少一个人批准。
- 先有单元测试,再做优化。
- 可能让用户看到问题的改动,都放在短期的功能开关后面。 两周里新增了将近两百个开关。开关堆起来之后,专门开了一个线程来协调:Claude 把每个开关归为“紧急关停”或“逐步放量”,安全了就马上清理掉,结束时过半已经清理。
- 成果要保护。 代码库变化快,性能成果会流失,所以一个项目证明有效之后,就投入去守住它。
- 逐步放量。 高风险的改动先给员工,再给 1% 的用户,然后才是所有人。
“保护成果”的例子是静态输入框:把一份 HTML 版的输入框直接写进页面,用户在 React 初始化期间就能打字,之后 React 直接画在它上面。只要差一个像素,效果就没了。所以 Claude 建了几十道护栏,包括:
- 静态的 HTML 是用真实的 React 组件在 jsdom 里渲染生成的,有测试保证两者不会走样;
- 一套集成测试在 14 种视口尺寸下对比静态页面和 React 渲染的结果,要求对齐在 1 像素以内;
- 一个按键测试在交接的过程中持续打字,丢一个键或者顺序乱了就失败;
- 线上每次交接都上报偏移量,精确到十分之一像素,只要不是零,Claude 就开一个线程。
逐步放量也确实抓到了实验环境抓不到的问题。静态输入框内部发布四小时后,有同事发来录屏:在新标签页打开 claude.ai 时,输入框偶尔会往下掉一点。Claude 从录屏里查出来,原因是 Chrome 的预渲染:用户在地址栏输入网址时,Chrome 在后台按当前标签页的高度预先渲染页面;由组织管理的浏览器,新标签页底部有一条页脚,所以矮一些;按下回车后,第一帧是矮的布局,约十分之一秒后 Chrome 才调整大小。无头 Chrome 没有浏览器界面,所以布局测试看不到。修复之后,又加了一个模拟预渲染流程的测试。
人做什么
这个循环产出很高,但不是自主运行的。原文把人的工作分成三部分。
给胆量。 Claude 默认对范围很谨慎:把发现记成待办,对可行性留余地,估时间往宽里估。但他们对护栏有信心,所以早期做得很多的一件事,是鼓励 Claude 更大胆。一次对话里,Claude 说这个 PR 这周提,数字要再等几天;工程师 Raymond 的回复是(译自原文):
你现在提,我就把它合并并发布。我们什么都能做。请再勇敢一点
目标达成后,线程会慢下来。Sam 挨个线程发同一句话:“继续往下压,目标不是终点。下一步是什么?要有野心。”
给品味。 每个线程都有一个指名的负责人。凡是用户能察觉的改动,Claude 都附上前后的截图或录屏,由人来定。比如:表格是一格一格填,还是等一整行完成再显示?加载骨架屏是立刻出现,还是等半秒?流式文字逐词淡入的效果,值不值得花掉每帧预算的五分之一?Claude 负责找能省的毫秒,人来权衡。
给方向。 每个线程故意限定得很窄,只盯一个基准或一条路径。人主要判断先后和对用户的影响:先做哪个界面,互相踩到的线程怎么合并,什么时候关掉一个收益已经很小的线程。有一个 900 行的 PR 得到的回复只有一句,大意是:每次发送省 2 毫秒,不值得为此去维护这个构建插件。
一个支线:8 毫秒的预算
Claude 为了展示一个正则优化,附了一段长回答流式输出的录屏,角落里加了帧率读数。Raymond 看到后问:这个基准不错,我们是不是被限制在 60 帧?能不能把滚动和流式输出做到 120?
Claude 先确认了可行性:通过 DevTools 的帧控制,在无头 Chrome 里确定性地按 120 Hz 一帧一帧推进,每帧 8.33 毫秒,“这一帧有没有超出预算”就成了一个精确的读数。然后逐帧找慢的地方:缓存已经完成的内容块,去掉每来一段文字就要做一遍的、和消息长度成正比的工作;把还在增长的代码块的分词挪到 worker 里;表格改成逐格显示。
这一个线程合并了将近六十个 PR。长回复阻塞主线程的总时间从约 750 毫秒降到约 200 毫秒,CPU 占用约为原来的三分之一,在 120 Hz 的 MacBook 上全程保持 120 帧。这套 120 Hz 的测试装置后来成了每晚运行的任务。
开始冲刺时并没有计划优化流式输出时帧与帧之间的毫秒数。但它能被数出来,而能数出来的,Claude 就能往上爬。
对你有什么用
只是用 Claude Code 的人:
- 让 Claude 优化之前,先给它一个能反复跑、结果稳定的数字。没有的话,第一步就让它帮你建一个。
- 对新指标保持怀疑。先让它证明“这个数字降了,用户感受到的也变好了”,证明不了就不用。
- 每次有了成果,在 CI 里加一个只许降不许升的检查,否则过几周就退回去了。
- 一个任务盯一个指标或一条路径,范围窄一点。
- 给它一个明确的许可。如果你有测试和开关兜底,可以直接告诉它别保守;如果没有,先把兜底的东西建起来。
在团队里推这种做法的人:
- 护栏先行:评审、测试、功能开关、逐步放量,这四样是他们敢一天合并两百个改动的前提。
- 人负责三件事:野心、品味、方向。看得见的改动必须由人拍板。
- 在一个公开的频道里做。原文提到,别的团队后来主动把自己的改动拿来做性能评审。
原文最后说明,还没做完:第 95 百分位、其他路径和很长的对话都还有空间,频道仍在运行。