我们如何在两周内让 claude.ai 快了 3 倍
深入我们的性能冲刺:Claude 构建的基准测试、每个 Slack 讨论串运行的循环,以及让我们安全发布 3,000 项变更的护栏。
今年八月,我们在为期两周的冲刺中,将 claude.ai 和 Claude 桌面应用的核心用户体验提速了约 3 倍。用户一直反馈说速度慢,他们说得没错。我们通过一个 Slack 频道统筹一切,每个讨论串中都有 Claude 参与。
我们聚焦于占用户活动 95% 的四个用户旅程。在第 75 百分位,claude.ai 全新加载后可输入页面的时间从 3.1 秒降至 0.55 秒,启动新的 Claude Code 会话从 0.8 秒降至 0.3 秒,加载 Claude Cowork 云会话从 2.6 秒降至 0.73 秒。总体而言,我们估计这每天为用户节省了数万小时的等待时间。
核心用户旅程,p75
真实用户监控,按平台和产品划分,8 月 13 日与 8 月 27 日对比
真实用户在第 75 百分位的十三项前后对比计时,于 8 月 13 日和 8 月 27 日测量,按旅程分组:启动应用(claude.ai 全新加载从 3,085 毫秒降至 550 毫秒,快 5.6 倍;桌面冷启动从 6,310 毫秒降至 3,328 毫秒,快 1.9 倍)、开始对话(快 1.5 至 2.4 倍)、加载对话(快 2.1 至 3.5 倍,其中 Cowork 云会话从 2,566 毫秒降至 728 毫秒)以及发送消息(快 2.2 至 19 倍,其中 Cowork 从 928 毫秒降至 48 毫秒)。四个旅程共十三项测量,前后对比:平均快 3.1 倍(几何平均值)。
我们使用了 Claude Tag(测试版),它运行着一个大致相当于 Opus 5.5 的内部研究模型。Claude 找出瓶颈、构建基准、发布改进,并监控每一次部署。我们通过设定目标、进行权衡取舍以及批准每一项变更来引导方向。通过这种方式,我们合并了三千多项变更,没有发生任何面向客户的事故或回滚。本文介绍了我们发布了什么、如何衡量,以及我们与 Claude 建立的安全执行闭环。
任务简报
冲刺开始前,我们创建了一个 Slack 频道,并附上以下 常设指令:
@Claude 你的职责是统筹与 claude.ai 网站和桌面应用性能相关的一切事务。你的职责包括监控部署以发现性能回归、评估现有遥测数据的准确性和全面性、维护精心整理的可观测性仪表板、主动实施针对已观察到的问题和低垂果实的解决方案、提出性能项目机会,以及与你的人类队友沟通。[…]
这个频道的最终目标是让你尽可能地自主运作,但目前我们知道这还不可能实现。
我们要求 Claude 通过 Datadog MCP 服务器分析使用数据。它识别出影响最大的四个用户旅程:启动应用、开始对话、加载现有对话以及发送消息。在网页端和桌面端之间,以及我们的各个产品中,这些旅程共对应十三项不同的测量。为了建立基线,我们添加了埋点,直到它们可以直接比较:每一项都始于用户交互,终于结果渲染完成,并区分了客户端和服务端的工作。
我们以大约二十个精心挑选的项目清单开启了冲刺,每个项目都针对特定的旅程。Claude 以毫秒为单位估算每个项目的影响,我们汇总这些估算来设定冲刺目标。其中一些项目相当大,但我们认为大概能在两周内完成大部分。
到第三天,我们就达成了十三个目标中的十二个。
计划中的项目提前落地。为了更快启动,我们将静态编辑器直接嵌入 HTML,使用户在 React 初始化期间即可输入,并预编译了 V8 代码缓存,让桌面应用外壳的主进程无需从头重新编译。为了更快导航,我们在会话之间保持编辑器挂载,在用户悬停时预取会话,并将侧边栏重渲染减少了 90%。
我们还为 Claude 留出了空间,让它识别机会并提出新的工作流。这些工作流很快发展成独立的完整项目,远超我们的初始目标。于是我们设定了新的目标,然后寻找更多可衡量的指标:
@Claude 我们最终资助了原始项目列表中的几乎每个项目,甚至更多。我们来做一次刷新 […] 我们还没有探索什么,我们可以在哪些方面进行爬山优化,此时最大的机会在哪里?[…] 我对古怪的想法持开放态度
任何东西都可以进行爬山优化
从一开始,我们就知道我们希望迭代速度超过部署节奏。Claude 可以异步工作许多小时,甚至通宵,而我们希望让它在无需等待现场读数的情况下验证其原型。为此,我们寻找了其他在实验室中衡量性能的方法。
Sam 找到了第一条线索:
#claude-apps-perf · 讨论串
Sam下午12:34
除了挂钟时间计时,我们还能做什么?例如,我们可以测量 JS 指令数吗?
ClaudeApp下午12:42
可以。对于纯 JS 热路径,直接统计指令数:在 Valgrind 下使用
node --predictable运行基准测试,并与已提交的基线进行比较——一次运行,无需统计。对于浏览器路径,Chromium 下没有指令计数,但有一系列其他确定性计数:每次交互的 React 提交数、来自 V8 精确覆盖率的函数调用数、布局和样式重算数、DOM 变更数。你想先要哪个?
Sam下午12:49
让我们在一个讨论串中探索 valgrind +
Ir+--predictable,并为每个浏览器/react 基准测试开新讨论串。在所有讨论串中都 ping 我。你知道我们想要什么。开始吧
十一分钟后,五个讨论串正在运行,每个都专注于不同的测量:指令数、V8 调用数、React 提交数、样式重算数和 DOM 变更数。
我们对每个新基准测试都持有一些怀疑态度。每个基准测试都有两项任务:首先,是 Claude 可以在实验室中改进的指标;其次,是 CI 中的护栏,其数字只能向下收紧。如果某个基准测试不稳定,或者它实际上与用户延迟不相关,我们会将其丢弃,而不是让 Claude 爬错山。
@Claude 请证明针对这些指标中的每一个进行爬山优化都能带来可衡量的挂钟性能提升。对于任何无法证明这一点的候选基准测试,我们将停止使用它们
挂钟时间是用户能感受到的,但它噪声很大,毫秒数太不稳定,无法用作 CI 门槛。指令数很有吸引力,因为它们是确定性的,但我们仍然需要 Claude 证明它们与挂钟时间相关。
于是我们要求 Claude 在两条热路径上降低计数:一条是组装会话消息树的例程,另一条是 Claude Code 输出中状态行的扫描器。Claude 使用 Valgrind 对两者进行了性能分析,发现第一条路径中有四分之一的指令是多态字典查找,分别解析了同一个消息 ID 三次。
一小时后,它将两条路径上的指令数分别削减了 48% 和 31%,实际耗时分别下降了 78% 和 44%。我们合入了两个新的棘轮机制。此后,任何提高这两条路径指令数的 PR 都会导致 CI 失败,而每日任务会在计数下降时下调各自的上限。
计数能反映时钟吗?
CPU 指令数与实际耗时
两条热点路径的前后对比柱状图:消息树组装将 CPU 指令数削减了 48%,实际耗时下降了 78%,速度提升 4.6 倍;状态行扫描器将指令数削减了 31%,实际耗时下降了 44%,速度提升 1.8 倍。两条热点路径,指令数对比实际耗时。计数在 Valgrind 下使用 node --predictable 测得;计时在同一基准测试下使用普通 node 测得,JIT 已预热。
这让我们得出了本次冲刺的核心教训。有了 Claude,测量某件事就变得可行了。
测量过去是第零步:你会添加一个指标,等待数据积累,然后才开始理解问题。而有了 Claude,它是攀登的第一步。一旦 Claude 有了要超越的数字,它就可以开始优化。这意味着我们能做的杠杆效应最大的事情,就是找到更多可以测量的东西。
循环,逐线程展开
所有这些都在同一个 Slack 频道中进行,多位工程师和 Claude 在每个线程里热烈讨论。由此,冲刺进入了一个循环:
- 有人会开一个线程,讨论旅程中某段缓慢的流程,通常附带截图或录屏。
- Claude 会追踪流程,然后找到或构建一个能复现该问题的基准测试。
- 一旦在实验室里取得了有希望的结果,Claude 就会带着 PR 回来——通常有多个,按风险和审查要求划分大小,任何用户可见的改动都放在功能开关后面。
- 发布后,Claude 会监控部署情况并读取现场数据。
- 如果性能有所提升,Claude 会通过下调基准测试来锁定胜果;如果没有,它会关闭功能开关并进行迭代。
- 然后它会去寻找同一旅程中的下一个缓慢点。
循环中的一个线程
有人开一个线程;Claude 从那里接手
单个线程运行的循环示意图:有人开一个线程讨论某段缓慢的流程,然后 Claude 构建基准测试,开启按审查要求划分大小的 PR,通过功能开关部署并按构建和平台读取现场数据;如果变快了,Claude 就下调基准测试,如果没变快,Claude 就关闭功能开关并迭代,无论如何都会继续寻找下一个缓慢点。
一个例子:有人分享了一段屏幕录制,显示侧边栏行在页面加载后才弹出。聊天和 Cowork 行在不同时间解析完成,让页面感觉卡顿。我们现有的监控都没有检测到它。我们最接近的是累计布局偏移,但每次偏移的得分只有约 0.008——远低于 0.1 的良好阈值。
Issac 提出了直接引用底层布局不稳定 API 的想法。Claude 创建了一个遥测事件,将每个sources的layout-shift映射到命名区域(例如侧边栏、转录)和阶段(例如首次绘制前、可输入后)。它添加了一个集成测试,打开一个带有已填充侧边栏的页面,将侧边栏的数据延迟到首次绘制之后才提供,并在任何命名区域发生任何偏移时失败。Claude 将此作为基准测试来证明修复效果:该测试在主分支上 20 次运行全部失败,在 PR 上 20 次运行全部通过。
事件部署后,Claude 读取了现场数据,发现 31% 的网页加载在页面可用之后又移动了某些内容,且没有任何用户交互。从那里开始,Claude 逐一找出原因:一个迟到的表头行、一个在用户名加载后横向滑动的光标、一个在滚动条出现时发生位移的列表。Claude 将最主要的几个问题作为一批一起修复,当它们被解决后,又找到了下一批。
那只是一个线程。在冲刺期间,我们同时运行了一百五十多个线程。
横向扩展
一旦这个循环在一个线程上跑通,运行更多线程就只是打开它们的问题。Claude 不会在原始请求完成后关闭线程,而是继续推进。单个线程会提交五十个、有时一百个优化 PR。越来越多地,是 Claude 而非我们中的任何一个人,去开启新线程以追查它自己发现的机会——这些机会来自单独的调查或夜间任务。频道中的一位工程师 Shelley 评论道:“[这个模型] 是个数字恶魔。”
每一次测量都能发现可改进之处。Claude 进行了一次 React hook 普查,发现在编辑器的输入路径中有 6,900 个 hook 和 900 个 store 订阅,每次按键都会触发重新渲染。Claude 统计了样式重算,发现单个 :root:has() 选择器让每次 DOM 变更增加了 24 毫秒。Claude 追踪了首次绘制后的代码路径,发现一个遗留的 location.reload() 导致每天五十万次隐藏重载,而我们的加载指标全都看不到。Claude 读取了空闲标签页的性能分析采样,发现相同的缓存快照每分钟被克隆两次写入 IndexedDB,全部发生在主线程上。
我们很少知道一个线程会通向哪里。在一次 CPU 卡顿排查中,Claude 注意到高亮一个已完成的代码块可能会让页面冻结约一秒。它深入实验室,找到了罪魁祸首:破折号。如果回复的 markdown 包含任何非 Latin-1 字符,比如破折号或弯引号,V8 会将整个字符串存储为 UTF-16,这使得每个语法高亮正则都走上了较慢的双字节路径。Claude 用二十行改动修复了它:在高亮之前,将每个代码块复制为单字节字符串。
高亮页面上的第一个代码块
主线程阻塞时间,修复前后
在实验室测量的柱状图,展示了在包含破折号的回复中高亮一个已完成代码块所需的主线程时间,对比将代码复制为单字节字符串的前后:页面上第一个 TypeScript 代码块从 1.0 秒降至 0.35 秒(减少 65%),该代码块之后的每次处理从 100 毫秒降至 40 毫秒。
到第二周时,我们几乎无法把产出总结成每日更新。在最繁忙的日子里,有超过两百项变更落地。Claude 不断提出新的基准测试;大约三分之一的 PR 包含额外的遥测或护栏,而每一个新工具都会带来更多线程和更多机会。
在一个频道中工作意味着一切都公开进行。我们互相进出彼此的线程,争论决策、庆祝胜利。消息传开了:其他团队开始把他们的变更带进频道,以进行性能审查。由于引入了所有护栏和 Claude 技能,新项目以细微但更高效的方式编写。
护栏
我们为这种节奏做好了准备。因为我们接触的几乎每样东西都是热路径(首次绘制、编辑器、转录文本),所以我们提前建立了安全机制。每个 PR 都要经过自动化审查并至少获得一个人工批准,单元测试始终先于优化,任何可能导致用户可见问题的功能都通过短期功能标志发布。
当功能标志开始堆积时,我们开了一个讨论串来协调它们的发布和清理。Claude 将每个标志分类为紧急关闭开关或渐进放量开关,并在安全后立即退役。两周内,我们引入了近两百个标志,其中超过一半在结束时已被清理。
我们也知道性能收益在快速演进的代码库中会衰减,而Anthropic 的代码发布速度很快。一旦某个项目证明了收益,我们就会投入资源保护它。例如,静态编辑器在设计上就是脆弱的。我们几乎立即向用户展示页面的 HTML 副本,然后让 React 直接在其上绘制。
如果 React 渲染哪怕差一个像素,魔法就消失了。于是 Claude 构建了数十个防护栏:
- 静态标记是通过在 jsdom 中渲染真实的 React 组件生成的,并有一个测试保证它们永不漂移。
- 一套集成测试套件在十四种视口尺寸下将静态页面与 React 渲染进行比较,并断言对齐误差在 1 px 以内。
- 按键测试直接穿过交接过程,任何丢失或乱序的按键都会导致失败。
- 在实际使用中,每次交接都会报告精确到十分之一像素的偏移。Claude 会为任何非零位移事件开一个讨论串。
并非所有问题都能在实验室中发现,因此我们还使用了最古老的防护栏:渐进发布。高风险变更先向员工发布,然后是百分之一的用户,最后是所有人。我们在内部发布静态编辑器四小时后,一位同事分享了一段屏幕录像,显示了一处我们所有指标都看不到的布局偏移。当他在新标签页中打开 claude.ai 时,编辑器会下坠——但那不是我们的代码。
#claude-apps-perf · 讨论串
Marius下午 6:15
偶尔,我在新标签页中打开 claude.ai 时会看到一个较小的(大约 15–20px)垂直布局偏移(把编辑器框往下推)(重新加载页面时不太明显)。我无法完全确定到底是什么引起的,但它确实存在
屏幕录像 · 0:02
ClaudeApp下午 7:05
在你的录像中找到了——是 Chrome 在调整页面大小,而不是从静态编辑器到真实编辑器的交接(今天你的 49 次加载中,该交接测得均为 0 px)。
- 在新标签页上,Chrome 会在页面下方绘制自己的 56 px 页脚(“Managed by anthropic.com · Customize Chrome”)。当标签页导航到 claude.ai 时,页脚消失,页面变高 56 px,但这是在我们首次绘制之后约 100 ms 才发生的。
- /new 将问候语和编辑器放在距页面顶部 18% 页面高度处,因此它们下坠 0.18 × 56 ≈ 10 px(你的视频测得 10)。重新加载没有需要移除的页脚,所以只在新标签页出现;“偶尔”取决于首次绘制是否赶在调整大小之前。
- 之所以只是“偶尔”,是因为它需要 Chrome 在你仍停留在新标签页时就从地址栏预渲染页面:隐藏的页面按较矮的高度布局,只有在显示之后才被调整大小。
- 这个问题一直存在,只是页面以前从未这么早绘制过。我们的布局测试无法发现它,因为无头 Chrome 没有可收起的浏览器 UI,而且占位符和应用是同步移动的。
不知怎的,Claude 将其追溯到 Chrome 预加载的一个边缘情况。当用户在地址栏中输入 URL 时,Chrome 会在后台以当前标签页的高度预渲染该页面。在由组织管理的浏览器上,新标签页因为页脚而略矮一些。当用户按下回车键时,claude.ai 的第一帧显示了这个略矮的布局,Chrome 在大约十分之一秒后将其调整大小。Claude 固定了跨调整大小的布局,我们还添加了一个测试来模拟预渲染流程。
引导
这个循环很有成效,但它并非自主运行的。让它保持快速、安全并走在正轨上是我们的工作,这包含三个部分。
雄心。默认情况下,Claude 对范围很谨慎。它会把发现的问题记录为工单,对可行性含糊其辞,并在估算中留出余量。但我们对自己的护栏很有信心。我们所做的很多工作,尤其是在早期,都是为了鼓励 Claude 更大胆一些。
#claude-apps-perf · 讨论串
ClaudeApp下午3:32
是的——一个小 PR,让 Code 拥有 Chat 和 Cowork 已有的相同计时标记。我这周会提交;实际上 Code 的数据要等几天才能合并、部署并建立基线窗口。
Raymond下午3:32
如果你现在就提交,我会让它合并并部署。我们有能力做任何事。请更大胆一些
ClaudeApp下午3:33
马上办——PR 一小时内就会提交。
当我们开始达成设定的目标时,我们注意到讨论串会慢下来。Sam 逐个讨论串发送了同样的消息:“让我们继续推动它下降,目标不是终点。下一步是什么?要有雄心。”
品味。每个讨论串都有指定的人类负责人,Claude 会用前后对比的截图或录屏向他们标出任何用户可感知的变化,由他们来裁决。表格应该逐个单元格填充,还是等每行都完成后再显示?加载骨架应该立即出现,还是半秒后才出现?流式文本上的逐词淡入效果是否值得花费帧预算的五分之一?Claude 寻找削减毫秒的方法,而我们权衡其中的取舍。
方向。我们刻意让每个讨论串保持狭窄,专注于一个基准或用户旅程,并要求 Claude 只在该范围内寻找改进。我们把这些讨论串看作一百五十把寻找钉子的锤子。我们的大多数决定都关乎排序和用户影响:优先处理哪些界面、如何合并相互冲突的讨论串,以及何时关闭一个已进入收益递减阶段的讨论串。一个 900 行的 PR 收到了一行回复:“我要裁定,每次发送节省 2 毫秒不值得为维护这个构建插件付出的复杂性。”
8 毫秒的预算
我们的一个支线任务展示了所有环节协同工作的效果。为了演示对实时语法高亮中使用的正则表达式的优化,Claude 附上了一段长答案在实验室中流式传输的屏幕录像。在角落,它添加了一个帧率读数,该读数在页面内根据动画帧时间戳计算得出。
#claude-apps-perf · 讨论串
Raymond下午2:33
这个基准测试其实挺厉害的。我们是不是被限制在 60 fps 了?你能不能试着把滚动和流式渲染的流畅度推到 120?据我所知你的机器可能不支持
ClaudeApp下午2:34
对,今天的机器跑在 60 Hz,因为无头 Chromium 默认就是这样。我相信它可以跑到 120(解除垂直同步限制或使用 DevTools 帧控制)——先确认这一点,然后我会针对 8.3 毫秒的帧预算重新运行评估。
ClaudeApp下午2:59
关于 120 Hz 机器的更新:可行。通过 DevTools begin-frame 控制,在无头 Chrome 中实现确定性的 120 Hz 帧步进——240 个 begin-frame 对应恰好 240 帧,每帧 8.33 毫秒,因此“这一帧是否符合 120 Hz 预算”就变成了精确的读数,而不是充满噪声的读数。
Raymond下午3:05
amazing
cook
在机制和雄心都确立之后,Claude 开始了工作。每个绘制帧都有 8.33 毫秒的预算,因此 Claude 逐帧推进一段长回复,为每一帧计时以找出缓慢的部分。它通过记忆化已完成的块消除了每个块的 O(message length) 工作,将不断增长的代码围栏的分词逻辑移到了 worker 中,并逐格显示表格。
在那一个线程中,我们合入了近六十个 PR。长回复阻塞主线程的总时间从原来的约 750 毫秒降到了约 200 毫秒,CPU 占用约为原来的三分之一,并且在一台 120 Hz 的 MacBook 上从头到尾保持 120 fps。120 Hz 机器本身也成了每晚运行的任务,由 Claude 监控回归。
ClaudeDevs@ClaudeDevs
Claude 网页版和桌面版上的长答案现在流式渲染流畅度提升约 4 倍。
我们重建了流式渲染器,只处理仍在变化的部分,因此在较慢的笔记本电脑上,长回复的卡顿减少了 9 倍,最严重的冻结时间缩短了 4.5 倍,而在一台 120Hz 的 MacBook 上,它从头到尾保持 120fps。
当我们开始这个冲刺时,我们并没有计划在流式传输过程中对帧与帧之间的毫秒数进行爬山优化。但事实证明我们能够计算它们——而任何我们能计算的东西,Claude 都能爬上去。
下一步
今天,claude.ai 和桌面应用比八月初快了约 3 倍,而这些棘轮机制应该能让它们保持在这个水平。但我们还没有完成:第 95 百分位、其他使用场景以及非常长的对话仍有改进空间。在另一篇文章中,我们还将介绍在冲刺期间带我们深入上游的一些支线任务,相关贡献已落地到 Electron、Chromium、Node.js 等项目中。
当我们在内部分享这些结果时,Issac 说得很到位:“即使在六个月前,你也不可能说服我相信这是可能的。”我们预计会继续以这种方式工作,一次一个线程,无论规模多大。这个频道依然活跃。
感谢 Alfred Xing、Anthony Morris、Benjamin Pasero、Chase McCoy、Joshua N.、Luke Deen Taylor、Marius Schulz 和 Shelley Vohr 的贡献。特别感谢 Boris Cherny 鼓励我们更有雄心。