Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 2 章 启动流程

“一个 CLI 工具的启动时间决定了它能否被程序员日常使用。每多 100 毫秒的等待,就多一份切换回浏览器的冲动。”

Claude Code 的启动流程是一个精心编排的异步序列。从用户敲下 claude 命令到出现交互式提示符,中间经历了入口分发、并行预取、配置加载、迁移执行、权限初始化等十余个阶段。本章将逐一剖析这些阶段,揭示工程团队在启动性能上所做的极致优化。

2.1 入口点:从 cli.tsx 到 main.tsx

Claude Code 的启动流程始于 src/entrypoints/cli.tsx。这个文件是编译后的二进制文件的真正入口。它的设计体现了快速路径优先的原则:

// src/entrypoints/cli.tsx
async function main(): Promise<void> {
  const args = process.argv.slice(2);

  // 快速路径:--version 零模块加载
  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }

  // 对于所有其他路径,加载启动分析器
  const { profileCheckpoint } = await import('../utils/startupProfiler.js');
  profileCheckpoint('cli_entry');

  // 快速路径:--dump-system-prompt
  if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') {
    // ...直接输出后退出
  }

  // 主路径:加载完整 CLI
  const { main } = await import('../main.js');
  await main();
}

注意 --version 的处理:它甚至不加载 startupProfiler 模块。MACRO.VERSION 是一个构建时内联的常量,这意味着 claude --version 的执行路径上零动态 import。这种极致的启动优化意识贯穿整个代码库。

另一个值得注意的是 cli.tsx 中的环境预设逻辑:

// 远程容器环境下增大 V8 堆上限
if (process.env.CLAUDE_CODE_REMOTE === 'true') {
  const existing = process.env.NODE_OPTIONS || '';
  process.env.NODE_OPTIONS = existing
    ? `${existing} --max-old-space-size=8192`
    : '--max-old-space-size=8192';
}

这段代码在任何模块加载之前就设置了内存限制,确保远程容器环境(Claude Code Remote,即 CCR)中的长时间会话不会因为默认的堆大小限制而 OOM。

graph TD
    A["cli.tsx<br/>二进制入口"] -->|"--version"| B["直接输出<br/>零 import"]
    A -->|"--dump-system-prompt"| C["加载最小依赖<br/>输出提示后退出"]
    A -->|"--claude-in-chrome-mcp"| D["启动 Chrome MCP<br/>独立服务"]
    A -->|"默认路径"| E["import main.tsx"]
    E --> F["main()"]
    F --> G["eagerLoadSettings()"]
    G --> H["Commander.js 解析"]
    H -->|"交互模式"| I["setup() → launchRepl()"]
    H -->|"-p/--print"| J["runHeadless()"]
    H -->|"mcp serve"| K["MCP 服务端"]

2.2 并行预取:与时间赛跑

main.tsx 文件的前 20 行是整个代码库中最精心设计的部分之一。它利用 JavaScript 的模块加载语义实现了零成本并行预取

// src/main.tsx — 文件最顶部
// 这些副作用必须在所有其他 import 之前运行:
// 1. profileCheckpoint 在模块求值开始前标记入口
// 2. startMdmRawRead 启动 MDM 子进程(plutil/reg query),
//    使其与后续约 135ms 的 import 并行运行
// 3. startKeychainPrefetch 并行启动两个 macOS keychain 读取
//    (OAuth + legacy API key),否则 isRemoteManagedSettingsEligible()
//    会在 applySafeConfigEnvironmentVariables() 内通过同步 spawn 顺序读取
//    (每次 macOS 启动约 65ms)

import { profileCheckpoint, profileReport } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();

import { ensureKeychainPrefetchCompleted, startKeychainPrefetch }
  from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();

这段代码利用了一个关键事实:ES 模块的 import 是同步求值的,但模块内部启动的异步操作会在后台并行执行

sequenceDiagram
    participant M as main.tsx 模块加载
    participant MDM as MDM 子进程
    participant KC as Keychain 读取
    participant Imports as 后续 import 链

    Note over M: profileCheckpoint('main_tsx_entry')
    M->>MDM: startMdmRawRead()
    Note over MDM: 异步:plutil/reg query
    M->>KC: startKeychainPrefetch()
    Note over KC: 异步:security find-generic-password
    M->>Imports: 继续同步 import 链<br/>(~135ms)

    Note over M,Imports: 并行执行

    Imports-->>M: 所有 import 完成
    MDM-->>M: MDM 数据就绪
    KC-->>M: Keychain 数据就绪

    Note over M: profileCheckpoint('main_tsx_imports_loaded')

关键洞察:在 macOS 上,startKeychainPrefetch() 并行启动了两个 keychain 读取操作(OAuth token 和 legacy API key)。如果不进行这个预取,这两个操作会在后续的 applySafeConfigEnvironmentVariables()同步串行执行,每次约 32ms,合计约 65ms。通过将它们提前到模块加载的最开始,这 65ms 被完全隐藏在后续 import 的 135ms 之内。

这种并行预取模式在后续也被多次使用。在 startDeferredPrefetches() 中,还有更多的后台预取操作被推迟到首次渲染之后:

// src/main.tsx
export function startDeferredPrefetches(): void {
  // 跳过条件:性能测量模式或 bare 模式
  if (isEnvTruthy(process.env.CLAUDE_CODE_EXIT_AFTER_FIRST_RENDER) ||
      isBareMode()) {
    return;
  }

  // 进程级预取(在用户输入第一条消息前完成)
  void initUser();
  void getUserContext();
  prefetchSystemContextIfSafe();
  void getRelevantTips();

  // 云提供商凭证预取
  if (isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK)) {
    void prefetchAwsCredentialsAndBedRockInfoIfSafe();
  }
  if (isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX)) {
    void prefetchGcpCredentialsIfSafe();
  }

  // 文件计数(ripgrep,3秒超时)
  void countFilesRoundedRg(getCwd(), AbortSignal.timeout(3000), []);

  // 分析与 feature flag
  void initializeAnalyticsGates();
  void prefetchOfficialMcpUrls();
  void refreshModelCapabilities();

  // 文件变更检测器
  void settingsChangeDetector.initialize();
  void skillChangeDetector.initialize();
}

这些预取操作全部使用 void 前缀启动(fire-and-forget),不阻塞 REPL 的首次渲染。它们的结果会被缓存,在后续的第一次 API 调用时消费。设计理念是:用户输入第一条消息通常需要几秒钟,这段时间足够完成所有预取操作

2.3 Feature Flag:编译时死代码消除

feature() 函数是 Claude Code 构建系统的核心机制之一。它来自 Bun 的 bun:bundle 模块,在编译期被求值为布尔常量:

import { feature } from 'bun:bundle'

// 编译时求值:对于外部构建,feature('COORDINATOR_MODE') === false
const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null

feature('COORDINATOR_MODE')false 时,Bun 的 bundler 会将整个三元表达式优化为 null,并且 require('./coordinator/coordinatorMode.js') 所引入的整个模块树都会被 tree-shake 移除。

src/tools.ts 中,这种模式被大量使用:

const SleepTool =
  feature('PROACTIVE') || feature('KAIROS')
    ? require('./tools/SleepTool/SleepTool.js').SleepTool
    : null

const cronTools = feature('AGENT_TRIGGERS')
  ? [
      require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
      require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
      require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
    ]
  : []

const WebBrowserTool = feature('WEB_BROWSER_TOOL')
  ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
  : null
graph LR
    subgraph "源码"
        S1["feature('X')<br/>? require(ModuleX)<br/>: null"]
    end

    subgraph "Bundler (feature X = true)"
        B1["require(ModuleX)"]
    end

    subgraph "Bundler (feature X = false)"
        B2["null"]
        B3["ModuleX 整个模块树<br/>被移除"]
    end

    S1 -->|"构建 ant 版本"| B1
    S1 -->|"构建 external 版本"| B2
    B2 -.->|"tree-shake"| B3

这种方法的优势对比运行时条件检查:

维度feature() 编译时消除process.env.X 运行时检查
产物大小未使用代码物理移除全部代码打包
运行时开销每次条件检查有分支预测成本
安全性内部代码不在产物中代码存在,可被逆向
调试需要不同构建产物同一产物,改环境变量即可

值得注意的是,process.env.USER_TYPE 的检查在代码中也大量存在,但它不是通过 feature() 实现的:

...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),

对比 feature() 的编译时消除,这里使用了字面量字符串比较。从 main.tsx 第 266 行可以看到一个线索:

if ("external" !== 'ant' && isBeingDebugged()) {
  process.exit(1);
}

这里的 "external" 是一个构建时被替换的宏 —— 在 ant 构建中它是 "ant",在外部构建中它是 "external"。同样地,process.env.USER_TYPE 的值也是在构建时确定的,使得 bundler 可以进行同样的死代码消除。

2.4 延迟加载:require() 的策略性使用

main.tsxtools.ts 中,大量使用了 require() 而非 import。这不是代码风格问题,而是一种刻意的延迟加载策略

// src/main.tsx — 打破循环依赖的延迟 require
const getTeammateUtils = () =>
  require('./utils/teammate.js') as typeof import('./utils/teammate.js');
const getTeammatePromptAddendum = () =>
  require('./utils/swarm/teammatePromptAddendum.js')
  as typeof import('./utils/swarm/teammatePromptAddendum.js');
// src/tools.ts — 打破循环依赖
const getTeamCreateTool = () =>
  require('./tools/TeamCreateTool/TeamCreateTool.js')
    .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js')
      .TeamCreateTool

这里有两种使用模式:

模式 1:打破循环依赖tools.ts 导入 TeamCreateTool,而 TeamCreateTool 的模块链最终又会导入 tools.ts。通过将 require() 包装在函数中,模块求值时不会立即触发导入,只在函数被调用时才执行,此时循环链中的所有模块已经完成初始化。

graph LR
    A["tools.ts"] -->|"静态 import"| B["TeamCreateTool"]
    B -->|"间接 import 链"| A

    style A fill:#ff6666
    style B fill:#ff6666
graph LR
    A["tools.ts"] -->|"延迟 require()"| B["TeamCreateTool"]
    B -->|"间接 import 链"| A

    style A fill:#66cc66
    style B fill:#66cc66

    Note["require() 在运行时<br/>被调用时,tools.ts<br/>已完成初始化"]

模式 2:条件加载 + 类型安全as typeof import(...) 语法确保了即使是动态 require(),返回值也有完整的类型信息:

const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
    as typeof import('./coordinator/coordinatorMode.js')
  : null;

这个模式同时实现了三个目标:

  1. 编译时死代码消除(通过 feature()
  2. 延迟加载(通过 require()
  3. 类型安全(通过 as typeof import(...)

2.5 启动性能:profileCheckpoint 系统

Claude Code 内建了一套启动性能打点系统,定义在 src/utils/startupProfiler.ts 中:

// src/utils/startupProfiler.ts
const DETAILED_PROFILING = isEnvTruthy(process.env.CLAUDE_CODE_PROFILE_STARTUP)
const STATSIG_SAMPLE_RATE = 0.005
const STATSIG_LOGGING_SAMPLED =
  process.env.USER_TYPE === 'ant' || Math.random() < STATSIG_SAMPLE_RATE

const SHOULD_PROFILE = DETAILED_PROFILING || STATSIG_LOGGING_SAMPLED

这个系统有两种工作模式:

  1. 采样日志模式:100% 的内部用户和 0.5% 的外部用户自动启用,将性能数据上报到 Statsig 用于聚合分析
  2. 详细分析模式:通过 CLAUDE_CODE_PROFILE_STARTUP=1 环境变量手动启用,输出完整的时间线报告
export function profileCheckpoint(name: string): void {
  if (!SHOULD_PROFILE) return

  const perf = getPerformance()
  perf.mark(name)

  // 仅在详细模式下捕获内存快照
  if (DETAILED_PROFILING) {
    memorySnapshots.push(process.memoryUsage())
  }
}

SHOULD_PROFILEfalse 时,profileCheckpoint() 是一个立即返回的空函数,零运行时开销。这意味着生产环境中 99.5% 的外部用户不会受到任何性能影响。

打点散布在整个启动路径中,形成了一条精确的时间线:

profileCheckpoint('main_tsx_entry')           // import 开始
// ... 135ms 的 import 链 ...
profileCheckpoint('main_tsx_imports_loaded')   // import 结束
profileCheckpoint('main_function_start')       // main() 函数入口
profileCheckpoint('main_warning_handler_initialized')
profileCheckpoint('eagerLoadSettings_start')   // 配置加载开始
profileCheckpoint('eagerLoadSettings_end')     // 配置加载结束

对应的阶段定义用于 Statsig 上报:

const PHASE_DEFINITIONS = {
  import_time: ['cli_entry', 'main_tsx_imports_loaded'],
  init_time: ['init_function_start', 'init_function_end'],
  settings_time: ['eagerLoadSettings_start', 'eagerLoadSettings_end'],
  total_time: ['cli_entry', 'main_after_run'],
} as const

当启用详细模式时,会生成一份包含内存快照的完整报告:

================================================================================
STARTUP PROFILING REPORT
================================================================================

    0ms    +0ms  profiler_initialized                    [RSS: 45MB, Heap: 12MB]
    1ms    +1ms  cli_entry                               [RSS: 45MB, Heap: 12MB]
    2ms    +1ms  main_tsx_entry                           [RSS: 46MB, Heap: 13MB]
  137ms  +135ms  main_tsx_imports_loaded                  [RSS: 89MB, Heap: 42MB]
  138ms    +1ms  main_function_start                      [RSS: 89MB, Heap: 42MB]
  ...
  312ms  +174ms  main_after_run                           [RSS: 112MB, Heap: 58MB]

Total startup time: 312ms
================================================================================

2.6 init 流程:从配置到就绪

main() 函数中的 init() 调用(定义在 src/entrypoints/init.ts)是启动序列中最关键的一步。它被 memoize 包装,确保全局只执行一次:

// src/entrypoints/init.ts
export const init = memoize(async (): Promise<void> => {
  const initStartTime = Date.now()
  profileCheckpoint('init_function_start')

  // 1. 启用配置系统 — 验证所有配置文件格式正确
  enableConfigs()
  profileCheckpoint('init_configs_enabled')

  // 2. 应用安全的环境变量(trust 对话框之前)
  applySafeConfigEnvironmentVariables()
  applyExtraCACertsFromConfig()
  profileCheckpoint('init_safe_env_vars_applied')

  // 3. 注册优雅退出处理
  setupGracefulShutdown()
  profileCheckpoint('init_after_graceful_shutdown')

  // 4. 初始化 1P 事件日志(异步,不阻塞)
  void Promise.all([
    import('../services/analytics/firstPartyEventLogger.js'),
    import('../services/analytics/growthbook.js'),
  ]).then(([fp, gb]) => {
    fp.initialize1PEventLogging()
    gb.onGrowthBookRefresh(() => {
      void fp.reinitialize1PEventLoggingIfConfigChanged()
    })
  })

  // 5. 填充 OAuth 账户信息
  void populateOAuthAccountInfoIfNeeded()

  // 6. 配置全局网络设置(mTLS + 代理)
  configureGlobalMTLS()
  configureGlobalAgents()
  profileCheckpoint('init_network_configured')

  // 7. API 预连接 — TCP+TLS 握手与后续工作并行
  preconnectAnthropicApi()

  // 8. 平台特定初始化
  setShellIfWindows()

  // 9. 远程管理设置加载(如果有资格)
  if (isEligibleForRemoteManagedSettings()) {
    initializeRemoteManagedSettingsLoadingPromise()
  }
  if (isPolicyLimitsEligible()) {
    initializePolicyLimitsLoadingPromise()
  }

  profileCheckpoint('init_function_end')
})
graph TD
    Init["init() 入口"] --> Configs["enableConfigs()<br/>验证配置文件"]
    Configs --> SafeEnv["applySafeConfigEnvironmentVariables()<br/>应用安全环境变量"]
    SafeEnv --> CACerts["applyExtraCACertsFromConfig()<br/>配置 CA 证书"]
    CACerts --> Shutdown["setupGracefulShutdown()<br/>注册退出处理"]
    Shutdown --> Analytics["初始化 1P 事件日志<br/>(异步)"]
    Analytics --> OAuth["填充 OAuth 信息<br/>(异步)"]
    OAuth --> MTLS["configureGlobalMTLS()<br/>配置 mTLS"]
    MTLS --> Proxy["configureGlobalAgents()<br/>配置代理"]
    Proxy --> Preconnect["preconnectAnthropicApi()<br/>TCP+TLS 预连接"]
    Preconnect --> Remote["远程管理设置<br/>(条件性加载)"]
    Remote --> Done["init 完成"]

    style Analytics fill:#ccddee
    style OAuth fill:#ccddee
    style Preconnect fill:#ccddee

    Note1["蓝色 = 异步非阻塞"]

其中有几个值得深入分析的设计决策:

applyConfigEnvironmentVariables 的分阶段执行

环境变量的应用被分为两个阶段:

  • applySafeConfigEnvironmentVariables():在 trust 对话框之前执行。只应用不涉及安全风险的环境变量(如代理设置、CA 证书路径)
  • applyConfigEnvironmentVariables():在 trust 建立之后执行。应用所有环境变量,包括可能影响安全行为的变量

这种分阶段设计确保了:在用户确认信任之前,不会执行任何可能被恶意项目配置利用的操作。

preconnectAnthropicApi 的预连接优化

// TCP+TLS 握手通常需要 100-200ms
// 在 CA 证书和代理配置完成后立即启动预连接
// 使其与后续 ~100ms 的初始化工作并行
preconnectAnthropicApi()

通过在配置网络参数后立即发起 TCP 连接,TLS 握手的延迟(100-200ms)被隐藏在后续的初始化工作中。这意味着当第一次 API 调用发生时,连接已经建立就绪。

迁移系统

main.tsx 中的 runMigrations() 负责执行配置迁移:

const CURRENT_MIGRATION_VERSION = 11;

function runMigrations(): void {
  if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
    migrateAutoUpdatesToSettings();
    migrateBypassPermissionsAcceptedToSettings();
    migrateEnableAllProjectMcpServersToSettings();
    resetProToOpusDefault();
    migrateSonnet1mToSonnet45();
    migrateLegacyOpusToCurrent();
    migrateSonnet45ToSonnet46();
    migrateOpusToOpus1m();
    migrateReplBridgeEnabledToRemoteControlAtStartup();

    if (feature('TRANSCRIPT_CLASSIFIER')) {
      resetAutoModeOptInForDefaultOffer();
    }
    if ("external" === 'ant') {
      migrateFennecToOpus();
    }

    saveGlobalConfig(prev =>
      prev.migrationVersion === CURRENT_MIGRATION_VERSION
        ? prev
        : { ...prev, migrationVersion: CURRENT_MIGRATION_VERSION }
    );
  }
}

迁移系统的设计特点:

  1. 版本门控:通过 migrationVersion 整数避免重复执行
  2. 幂等性:每个迁移函数必须是幂等的(多次执行结果一致)
  3. 全量执行:不像数据库迁移那样逐版本递增,而是每次检查是否需要迁移时运行所有迁移。这简化了实现 —— 每个迁移函数自行检查是否需要执行
  4. 异步迁移分离migrateChangelogFromConfig() 是异步的,使用 fire-and-forget 模式,不阻塞启动

模型字符串的迁移链清晰记录了 Claude 模型的演进历史:Fennec → Opus → Legacy Opus → Sonnet 1M → Sonnet 4.5 → Sonnet 4.6 → Opus 1M。每次模型更名都需要迁移用户存储的模型偏好设置。

安全前置检查

main() 函数的开头包含了一个安全检查:

// src/main.tsx
// 安全:防止 Windows 从当前目录执行命令
process.env.NoDefaultCurrentDirectoryInExePath = '1';

以及反调试检测:

// 外部构建中检测并阻止调试器附加
if ("external" !== 'ant' && isBeingDebugged()) {
  process.exit(1);
}

isBeingDebugged() 检查 --inspect 标志、NODE_OPTIONS 环境变量和活跃的 Inspector 连接。在外部构建中,调试模式被直接禁止。这是一种安全纵深防御措施,防止通过调试器绕过安全检查。

2.7 Commander.js 命令注册

main() 函数中使用 Commander.js 定义了完整的 CLI 接口。命令定义代码非常长(main.tsx 超过 4000 行),但架构模式是统一的。主命令处理交互模式,子命令处理各种非交互操作。

在进入 Commander 解析之前,有几个特殊路径通过直接的 argv 检查提前处理:

// cc:// URL 协议处理
if (feature('DIRECT_CONNECT')) {
  const ccIdx = rawCliArgs.findIndex(
    a => a.startsWith('cc://') || a.startsWith('cc+unix://')
  );
  if (ccIdx !== -1 && _pendingConnect) {
    // 重写 argv,让主命令处理器接管
    _pendingConnect.url = parsed.serverUrl;
    _pendingConnect.authToken = parsed.authToken;
    process.argv = [process.argv[0]!, process.argv[1]!, ...stripped];
  }
}

// `claude assistant [sessionId]` 特殊处理
if (feature('KAIROS') && _pendingAssistantChat) {
  if (rawArgs[0] === 'assistant') {
    // ...
  }
}

// `claude ssh <host> [dir]` 特殊处理
if (feature('SSH_REMOTE') && _pendingSSH) {
  if (rawCliArgs[0] === 'ssh') {
    // 提取 --permission-mode, --local 等 SSH 特定标志
    // ...
  }
}

这些特殊路径通过修改 process.argv 来“注入“参数,使得 Commander.js 的主命令处理器能够统一处理。这种设计避免了在 Commander 中注册大量子命令,同时保持了主命令的交互式 TUI 体验。

本章小结

Claude Code 的启动流程体现了一套完整的性能工程方法论:

  1. 快速路径优先--version 零 import,特殊入口延迟加载
  2. 并行预取:利用模块加载窗口并行执行 I/O 操作
  3. 编译时优化feature() 实现物理级别的代码消除
  4. 分阶段初始化:安全敏感操作在 trust 建立后才执行
  5. 延迟到首次使用:预取操作的结果在第一次需要时才被消费
  6. 可观测性内建:profileCheckpoint 系统以零成本(非采样用户)实现启动性能监控

这些优化的叠加效果是:在典型的 macOS 环境下,从命令输入到终端就绪的时间控制在 300ms 左右,达到了“瞬间响应“的体验阈值。