Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

09 — 启动性能优化

Claude Code 把“CLI 启动快不快”当成一等问题,而不是运行起来以后再说

为什么一个命令行工具还要这么在意启动

想象一下这个场景:

你正在调试一个 bug,需要快速查看某个文件的内容。你打开终端,输入 claude "帮我看看 auth.ts 里的问题",然后...等待...等待...3 秒后,Claude Code 才开始响应。

3 秒听起来不长,但在这个场景下,它足以让你失去耐心。

因为 Claude Code 不是后台服务,它是高频交互型工具。用户会:

  • 频繁打开新终端
  • 在不同项目间切换
  • 做短平快的单次调用
  • 快速进入���退出会话

每次启动都要等几秒,体验会迅速崩溃。

这就像你每次打开冰箱都要等 3 秒门才能开——理论上可以接受,实际上会让你抓狂。

所以对 Claude Code 来说,启动阶段的几十毫秒、上百毫秒,不是可以忽略的小数点,而是用户是否愿意持续使用的重要门槛。

更深一层:启动慢会破坏工作流

启动慢不只是"等一下"的问题,它会破坏整个工作流:

  • 打断思路:你正在思考问题,启动慢会让你从"心流状态"中被拉出来
  • 降低使用频率:如果每次启动都慢,你会下意识地减少使用次数
  • 影响信任感:启动慢会让人觉得"这个工具很重",进而怀疑它的可靠性

这就是为什么 Claude Code 把"CLI 启动快不快"当成一等问题,而不是"运行起来以后再说"。

本章关键源码入口

文件 作用
src/main.tsx 顶层并行预热、preAction 等待点、启动路径上的预取动作
src/utils/startupProfiler.ts profileCheckpoint() / profileReport():启动阶段打点与报告
src/entrypoints/init.ts 安全环境变量应用、telemetry 延后初始化等启动期时序控制
src/tools.ts 条件导入和工具加载策略

新手先记住:启动优化不是只有“少 import”

Claude Code 的启动优化可以概括成 3 类手段:

  • 尽早并行做 I/O
  • 尽量延后加载重模块
  • 在构建时删除根本用不到的代码

它们分别对应:

  • 并行预取
  • 懒加载
  • Feature Flag 驱动的死代码消除

main.tsx 开头为什么那么“奇怪”

如果你打开 src/main.tsx,一开始会看到很显眼的注释和顶层副作用调用:

  • 先打 profile checkpoint
  • 先启动 MDM 原始读取
  • 先启动 keychain 预取

这看起来有点反直觉,因为很多工程都会避免顶层副作用。

但 Claude Code 这里是刻意为之:

它想把某些慢 I/O 提前发车,让它们和后续模块加载并行进行。

这是一种非常实用的 CLI 启动优化技巧。

从源码注释还能再看得更具体一点:

  • profileCheckpoint('main_tsx_entry') 先把入口打点立住
  • startMdmRawRead() 先把 MDM 相关子进程发出去
  • startKeychainPrefetch() 先把 keychain 读取发出去

后面真正执行命令前,再通过 preAction 把这些提前发出的异步结果统一等回来。
这不是“乱用顶层副作用”,而是有意识地把关键 I/O 从串行改成并行。

并行预取到底在优化什么

并行预取的核心思想不是“更早拿到结果”,而是:

不要等所有 import 都结束后,才开始做本来就注定要做的 I/O。

例如:

  • 读取 MDM 设置
  • 读取 keychain 里的认证信息
  • 某些配置或远程能力的预判

如果这些动作晚做,就会把本来可以并行摊平的等待时间硬串成一条长链。

而且并行预取不只发生在开头那两个例子里。
main.tsx 的启动路径还能看到一些典型预取:

  • Bedrock / GCP 凭据预取
  • Official MCP URL 预取
  • Analytics gates 初始化
  • model capabilities 刷新

这说明 Claude Code 的启动优化不是单点技巧,而是一整套“能提前的就提前,能并行的就并行”的策略。

懒加载不只是为了快,也为了控制复杂度

Claude Code 源码里大量使用 require(...) 的惰性加载方式。

这背后通常有两个目的:

1. 启动时先别把重量级模块全拉起来

有些功能不是每次启动都立刻用到,例如:

  • 某些 Assistant/KAIROS 相关模块
  • 协调器模式
  • 某些高级命令或实验功能

如果一上来全加载,启动成本会被无谓抬高。

2. 避免循环依赖

在一个大型 CLI 里,入口、工具、命令、状态、远程能力之间很容易互相引用。
惰性加载不仅省时间,也是在拆解初始化时序上的耦合。

Bun 的 feature() 为什么对启动这么重要

Claude Code 大量使用 feature('FLAG') 这类写法。

这不只是运行时条件判断,更重要的是:

在构建阶段,未启用的分支可以被直接删掉。

这样带来的收益包括:

  • 更小的构建体积
  • 更少的模块求值
  • 更低的启动时内存与解析成本

所以 Feature Flag 在 Claude Code 里不仅服务产品灰度,也服务性能工程。

启动优化为什么不能只靠“最后测一下”

Claude Code 在启动路径上专门做了 profiler checkpoint,这说明团队并不是凭感觉在优化。

它会关注:

  • 哪一段 import 最重
  • 哪个 I/O 可以提前并行
  • 哪些功能值得懒加载
  • 某次改动是否把启动路径又拖慢了

这类工具化意识很重要,因为启动优化最怕“今天快了,明天又慢回去”。

startupProfiler.ts 可以直接看出这一点:

  • checkpoint 会被持续记录
  • profiling report 会把时间线写出来
  • 团队不是凭感觉猜“哪里慢”,而是先把关键路径打成可观察对象

为什么 CLI 的启动优化和 Web 首屏优化很像

如果你做过前端,会发现它们的思路很像:

  • 都要区分关键路径和非关键路径
  • 都要尽量推迟非必要模块
  • 都要把可以并行的操作尽量并行
  • 都要持续打点,而不是靠主观感觉

区别只是一个发生在浏览器,一个发生在终端。

Claude Code 为什么能在启动阶段就体现出整体架构水平

因为启动阶段会强迫系统暴露很多真实问题:

  • 模块是不是耦合过深
  • 谁依赖谁
  • 哪些能力必须前置
  • 哪些能力其实可以延后
  • 哪些实验功能不该污染核心路径

一个项目如果启动路径写得乱,往往意味着整体边界也不够清楚。

Claude Code 的 main.tsx 值得看的地方,正是在于它把这类问题处理得相当有意识。

还有一个容易被忽视的点:
启动优化和安全边界在这里也交织在一起。

例如 init.ts 会先走 applySafeConfigEnvironmentVariables(),之后等远程设置等条件满足,再补 applyConfigEnvironmentVariables()
这说明启动阶段不是一味追快,而是在“尽快启动”和“不要太早执行高风险配置”之间做权衡。

新手可以从这一章学到什么

如果你以后也要做大型 CLI,可以直接借鉴这些原则:

1. 启动最先走的路径必须极度克制

不要让次要能力污染主入口。

2. I/O 要尽可能并行

能早发车的慢操作就别等。

3. 重模块尽量后置

尤其是实验功能、少用功能和大依赖。

4. 性能优化需要配合结构优化

如果模块边界一团乱,懒加载和并行化都会很难做。

5. 启动优化经常和 Feature Flag 共用同一套结构

很多顶层条件导入本质上同时在服务两件事:

  • 产品实验
  • 主路径减负

新手常见误区

误区 1:CLI 启动慢一点无所谓

不对。高频交互型 CLI 的体感非常依赖启动速度。

误区 2:启动优化就是减少几行 import

不对。真正有效的是关键路径梳理、I/O 并行、懒加载和构建期裁剪的组合。

误区 3:性能问题后面再测

不对。等架构长歪了再补,成本会高很多。

本章小结

Claude Code 的启动优化体现出很成熟的工程意识:

  • 把启动阶段当成一级性能场景
  • 用顶层并行预取缩短关键路径
  • 用惰性加载降低初始化负担
  • 用构建期 Feature Flag 做死代码消除
  • 用 profiler 持续量化而不是凭感觉优化
  • 把“安全可提前做什么、完整配置何时再补”也纳入启动时序设计

下一步