启动流程详解
一个程序的启动流程,往往暴露它最核心的设计决策。Claude Code 的启动链路走下来,你会发现这帮人对性能优化是认真的。
启动链路总览
从你敲下 claude 到看见输入框,经过 3 个关键文件:
用户执行 claude
|
cli.tsx (303行) -- 巨型路由器,所有模块延迟加载
|
main.tsx (4684行) -- Commander.js 选项解析 + 迁移
|
init.ts (309行) -- 多步骤初始化,memoize 保证只跑一次
|
REPL.tsx -- React 组件挂载,交互循环开始第一站:cli.tsx -- 命令行路由
cli.tsx 的 main() 函数本质上是一个巨型 switch-case。但有个关键设计:所有依赖都用动态 import()。
为什么?因为 Claude Code 有十几种运行模式——普通 REPL、管道模式、MCP 服务器、Bridge、Daemon……每种模式需要的模块完全不同。如果在入口全部 import,启动时间直接爆炸。
路由表(部分)
| 条件 | 目标 | 说明 |
|---|---|---|
--version | 直接输出 | 零模块加载快速路径 |
--mcp | MCP 服务器 | feature gate 控制 |
-p / --pipe | 管道模式 | 一次性查询 |
bridge / remote | Bridge 模块 | 远程协作 |
daemon | 守护进程 | 后台运行 |
| 默认 | main.tsx | 交互式 REPL |
这种路由设计我在很多 CLI 工具里见过,但做到这个粒度的不多。16+ 个 feature-gated 快速路径,每个都是独立的动态 import。
第二站:main.tsx -- 配置解析与迁移
4684 行的 main.tsx 是主力文件,干两件事:
1. Commander.js 选项解析
大约 50 个 CLI 选项:
typescript
program
.option('-p, --pipe', '管道模式')
.option('--model <model>', '模型选择')
.option('--max-turns <n>', 'Agent 最大轮次')
.option('--permission-mode <mode>', '权限模式')
.option('--resume <session>', '恢复会话')
.option('--output-format <fmt>', '输出格式')
.option('--verbose', '详细日志')2. 迁移系统
版本号已经到 11 了,说明经历过不少次配置格式变更:
typescript
const CURRENT_MIGRATION_VERSION = 11迁移包括 9 个同步迁移、2 个条件迁移、1 个异步迁移。用户升级后旧配置自动迁移,不用手动改。
第三站:init.ts -- 初始化序列
init() 被 memoize 包裹,保证只执行一次:
1. enableConfigs() -- 启用配置系统
2. applySafeConfigEnvVars() -- 安全环境变量
3. setupGracefulShutdown() -- 注册 SIGINT/SIGTERM 清理
4. 初始化事件日志 + GrowthBook -- 特性门控就绪
5. populateOAuthAccountInfo() -- OAuth 账户信息
6. initJetBrainsDetection() -- IDE 检测
7. detectCurrentRepository() -- Git 仓库检测
8. initRemoteManagedSettings() -- 远程配置加载
9. recordFirstStartTime() -- 首次启动时间
10. configureGlobalMTLS() -- mTLS 证书
11. configureGlobalAgents() -- 代理配置
12. preconnectAnthropicApi() -- API 预连接注意第 12 步——在用户还没输入任何东西时,就先和 API 建立 TCP 连接。等用户真正发消息时,少一次握手延迟。
延迟遥测
OpenTelemetry SDK 约 400KB,等用户接受信任对话框后才加载。遥测不影响核心功能,没必要拖慢启动。
第四站:REPL.tsx -- 组件树
jsx
<REPL>
<App>
<FpsMetricsProvider>
<StatsProvider>
<AppStateProvider>
<FullscreenLayout>
<ScrollBox>
<LogoHeader />
<VirtualMessageList />
</ScrollBox>
<StatusLine />
<PromptInput />
</FullscreenLayout>
</AppStateProvider>
</StatsProvider>
</FpsMetricsProvider>
</App>
</REPL>启动时序
t=0ms cli.tsx 开始
t=5ms -- 路由判断
t=10ms -- 动态 import main.tsx
t=25ms main.tsx Commander.js 解析
t=30ms -- 迁移系统执行
t=50ms init() 开始
t=200ms -- API 预连接
t=220ms REPL.tsx 挂载
t=300ms -- Ink 渲染引擎启动
t=350ms -- 首帧渲染
t=400ms -- 等待用户输入从 0 到可交互,400ms。对一个 1500+ 文件的 TypeScript 项目来说,这个数字相当不错。
启动优化总结
| 技术 | 效果 |
|---|---|
| 动态 import | 只加载当前模式需要的模块 |
| memoize 初始化 | 每个步骤只跑一次 |
| API 预连接 | 省掉首次请求的 TCP 握手 |
| 延迟遥测 | 400KB OTEL 不阻塞启动 |
| 并行 MCP | 多个 MCP 服务器同时连接 |
| 渐进渲染 | 先画框架,数据后填 |
这些技术单个都不新鲜,但组合在一起,效果立竿见影。