Claude Code Wiki
首页 深入解析 开发实践

性能优化:启动性能与运行时优化技巧

高级 开发实践

Claude Code CLI 作为终端交互式 AI 助手,性能直接影响用户体验。本文档深入解析项目在启动性能运行时优化两方面的工程实践,涵盖延迟加载、并行预取、智能缓存、虚拟滚动等核心技术。通过系统性的性能分析工具和优化策略,实现了亚秒级启动和流畅的终端交互体验。

性能分析体系

启动性能分析器

项目采用分层性能追踪策略,通过 startupProfiler 实现两种模式的性能监控:

采样模式:100% 内部用户 + 0.5% 外部用户自动采样,通过 Statsig 记录关键阶段耗时。采样决策在模块加载时一次性确定,非采样用户无需承担性能分析开销。

详细模式:通过 CLAUDE_CODE_PROFILE_STARTUP=1 环境变量启用,记录完整时间线与内存快照,生成详细报告文件。

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

const DETAILED_PROFILING = isEnvTruthy(process.env.CLAUDE_CODE_PROFILE_STARTUP)
const SHOULD_PROFILE = DETAILED_PROFILING || STATSIG_LOGGING_SAMPLED

性能检查点通过 profileCheckpoint() 函数记录,内部使用 Node.js perf_hooks API 获取高精度时间戳:

export function profileCheckpoint(name: string): void {
  if (!SHOULD_PROFILE) return
  
  const perf = getPerformance()
  perf.mark(name)
  
  if (DETAILED_PROFILING) {
    memorySnapshots.push(process.memoryUsage())
  }
}

报告生成时计算每个阶段的增量耗时和内存使用:

function getReport(): string {
  const marks = perf.getEntriesByType('mark')
  let prevTime = 0
  
  for (const [i, mark] of marks.entries()) {
    lines.push(
      formatTimelineLine(
        mark.startTime,
        mark.startTime - prevTime,  // 增量时间
        mark.name,
        memorySnapshots[i],
        8, 7  // 对齐宽度
      )
    )
    prevTime = mark.startTime
  }
}

关键阶段定义包括导入时间(import_time)、初始化时间(init_time)、设置加载时间(settings_time)和总启动时间(total_time)。

Sources: src/utils/startupProfiler.ts

查询性能分析器

queryProfiler 追踪从用户输入到首 token 到达的完整查询流水线,通过 CLAUDE_CODE_PROFILE_QUERY=1 启用。追踪的关键检查点包括:

  • 查询准备阶段:上下文加载、微压缩、自动压缩检查
  • API 请求阶段:客户端创建、Schema 构建、消息标准化
  • 响应处理阶段:首 chunk 接收(TTFT)、流式完成、工具执行
// src/utils/queryProfiler.ts
export function startQueryProfile(): void {
  const perf = getPerformance()
  perf.clearMarks()
  memorySnapshots.clear()
  queryCount++
  queryCheckpoint('query_user_input_received')
}

性能报告自动识别慢操作(>100ms)并生成警告:

function getSlowWarning(deltaMs: number, name: string): string {
  if (deltaMs > 1000) return ` ⚠️  VERY SLOW`
  if (deltaMs > 100) return ` ⚠️  SLOW`
  
  if (name.includes('git_status') && deltaMs > 50) return ' ⚠️  git status'
  if (name.includes('tool_schema') && deltaMs > 50) return ' ⚠️  tool schemas'
}

报告计算 TTFT(Time To First Token)并分解为预请求开销和网络延迟:

const preRequestOverhead = apiRequestSentTime
const networkLatency = firstChunkTime - apiRequestSentTime
const preRequestPercent = ((preRequestOverhead / firstChunkTime) * 100).toFixed(1)

Sources: src/utils/queryProfiler.ts

FPS 追踪器

FpsTracker 监控终端渲染性能,记录每帧渲染时长并计算平均 FPS 和 P99 低帧率:

// src/utils/fpsTracker.ts
export class FpsTracker {
  private frameDurations: number[] = []
  
  record(durationMs: number): void {
    this.frameDurations.push(durationMs)
  }
  
  getMetrics(): FpsMetrics {
    const averageFps = totalFrames / (totalTimeMs / 1000)
    
    const sorted = this.frameDurations.slice().sort((a, b) => b - a)
    const p99FrameTimeMs = sorted[p99Index]!
    const low1PctFps = 1000 / p99FrameTimeMs  // 最慢 1% 帧的 FPS
    
    return { averageFps, low1PctFps }
  }
}

Sources: src/utils/fpsTracker.ts

启动性能优化策略

并行预取与预连接

API 预连接:通过 preconnectAnthropicApi() 在初始化期间提前执行 TCP+TLS 握手,将 100-200ms 的网络延迟与后续的命令行解析和配置加载并行化:

// src/utils/apiPreconnect.ts
export function preconnectAnthropicApi(): void {
  const baseUrl = process.env.ANTHROPIC_BASE_URL || getOauthConfig().BASE_API_URL
  
  void fetch(baseUrl, {
    method: 'HEAD',  // 无响应体,连接立即可复用
    signal: AbortSignal.timeout(10_000),
  }).catch(() => {})
}

预连接在配置加载和全局代理设置完成后触发,确保使用正确的传输层。当检测到代理、mTLS 或 Unix socket 配置时跳过,因为这些场景下 SDK 使用自定义 dispatcher 不会复用全局连接池。

Sources: src/utils/apiPreconnect.ts

Keychain 并行预取:macOS 平台上,OAuth token 和 legacy API key 的 keychain 读取原本是顺序执行(各约 32-33ms,总计 65ms)。通过在 main.tsx 顶层并行启动两个子进程,与模块导入阶段重叠:

// src/utils/secureStorage/keychainPrefetch.ts
export function startKeychainPrefetch(): void {
  const oauthSpawn = spawnSecurity(getMacOsKeychainStorageServiceName(CREDENTIALS_SERVICE_SUFFIX))
  const legacySpawn = spawnSecurity(getMacOsKeychainStorageServiceName())
  
  prefetchPromise = Promise.all([oauthSpawn, legacySpawn]).then(([oauth, legacy]) => {
    if (!oauth.timedOut) primeKeychainCacheFromPrefetch(oauth.stdout)
    if (!legacy.timedOut) legacyApiKeyPrefetch = { stdout: legacy.stdout }
  })
}

预取结果缓存在内存中,后续的同步读取直接命中缓存,无需再次调用 security 命令。

Sources: src/utils/secureStorage/keychainPrefetch.ts

延迟加载策略

OpenTelemetry 延迟加载:遥测系统涉及大量依赖(OpenTelemetry SDK + protobuf 约 400KB,gRPC exporter 约 700KB),通过动态 import() 延迟到实际需要时加载:

// src/entrypoints/init.ts
async function setMeterState(): Promise<void> {
  const { initializeTelemetry } = await import(
    '../utils/telemetry/instrumentation.js'
  )
  // gRPC exporters 进一步延迟到 instrumentation.ts 内部
}

1P 事件日志系统同样延迟加载以避免启动时加载 OpenTelemetry sdk-logs/resources:

void Promise.all([
  import('../services/analytics/firstPartyEventLogger.js'),
  import('../services/analytics/growthbook.js'),
]).then(([fp, gb]) => {
  fp.initialize1PEventLogging()
})

Sources: src/entrypoints/init.ts

Zod Schema 延迟构造:通过 lazySchema 工厂函数将 Schema 构造从模块初始化时间推迟到首次访问:

// src/utils/lazySchema.ts
export function lazySchema<T>(factory: () => T): () => T {
  let cached: T | undefined
  return () => (cached ??= factory())
}

// 使用示例
export const SandboxSettingsSchema = lazySchema(() =>
  z.object({
    network: SandboxNetworkConfigSchema(),
    filesystem: SandboxFilesystemConfigSchema(),
  })
)

Sources: src/utils/lazySchema.ts

模块导入优化

main.tsx 通过精心设计的导入顺序实现并行化:

// 1. 立即标记入口点
profileCheckpoint('main_tsx_entry');

// 2. 启动 MDM 设置读取(子进程并行运行)
startMdmRawRead();

// 3. 启动 keychain 预取(子进程并行运行)
startKeychainPrefetch();

// 4. 同步导入阶段(与上述子进程并行)
import { feature } from 'bun:bundle';
import { Command } from '@commander-js/extra-typings';
// ... 其他导入

Sources: src/main.tsx

预取系统上下文

对于非交互式会话或已建立信任的交互式会话,系统提前获取上下文以加速首次查询:

function prefetchSystemContextIfSafe(): void {
  const isNonInteractiveSession = getIsNonInteractiveSession();
  
  if (isNonInteractiveSession) {
    void getSystemContext();
    return;
  }
  
  const hasTrust = checkHasTrustDialogAccepted();
  if (hasTrust) {
    void getSystemContext();
  }
}

Sources: src/main.tsx

运行时性能优化

虚拟滚动

长对话场景下,渲染数千条消息会导致巨大的内存开销(每条 MessageRow 约 250KB RSS)和 CPU 成本。useVirtualScroll Hook 实现 React 层虚拟化,仅渲染视口 + 过扫区域内的消息:

// src/hooks/useVirtualScroll.ts
export function useVirtualScroll(
  scrollRef: RefObject<ScrollBoxHandle | null>,
  itemKeys: readonly string[],
  columns: number,
): VirtualScrollResult {
  const DEFAULT_ESTIMATE = 3  // 未测量项的估计高度(故意低估)
  const OVERSCAN_ROWS = 80    // 视口上下额外渲染的行数
  const MAX_MOUNTED_ITEMS = 300  // 最大挂载项数上限
  const SCROLL_QUANTUM = OVERSCAN_ROWS >> 1  // 滚动量化阈值

高度估算与缓存:未测量的消息使用固定估算值(3 行),首次渲染后通过 Yoga 布局获取真实高度并缓存。终端宽度变化时,缓存高度按比例缩放而非清空,避免重新挂载大量消息:

if (prevColumns.current !== columns) {
  const ratio = prevColumns.current / columns
  for (const [k, h] of heightCache.current) {
    heightCache.current.set(k, Math.max(1, Math.round(h * ratio)))
  }
}

滚动量化:为避免每次滚轮事件(每刻度 3-5 次)触发完整 React 提交,使用 SCROLL_QUANTUM(40 行)作为重新计算挂载范围的阈值。视觉滚动保持流畅(ScrollBox 直接操作 DOM),React 仅在挂载范围需要移动时重新渲染。

渐进式挂载:快速滚动到未缓存区域时,单次最多挂载 25 个新项(SLIDE_STEP),避免一次性挂载数百项导致的数百毫秒阻塞:

const SLIDE_STEP = 25  // 单次提交最多挂载的新项数

Sources: src/hooks/useVirtualScroll.ts

Ink 渲染优化

Ink 框架的渲染优化器通过 optimize() 函数合并和消除冗余的终端补丁:

// src/ink/optimizer.ts
export function optimize(diff: Diff): Diff {
  const result: Diff = []
  
  for (const patch of diff) {
    // 跳过无操作:空 stdout、(0,0) 光标移动、count=0 的清屏
    if (type === 'stdout' && patch.content === '') continue
    if (type === 'cursorMove' && patch.x === 0 && patch.y === 0) continue
    if (type === 'clear' && patch.count === 0) continue
    
    // 合并连续的光标移动
    if (type === 'cursorMove' && lastType === 'cursorMove') {
      result[lastIdx] = {
        type: 'cursorMove',
        x: last.x + patch.x,
        y: last.y + patch.y,
      }
      continue
    }
    
    // 拼接相邻的样式补丁(ANSI 转义序列)
    if (type === 'styleStr' && lastType === 'styleStr') {
      result[lastIdx] = { type: 'styleStr', str: last.str + patch.str }
      continue
    }
    
    // 取消光标隐藏/显示配对
    if ((type === 'cursorShow' && lastType === 'cursorHide') ||
        (type === 'cursorHide' && lastType === 'cursorShow')) {
      result.pop()
      continue
    }
  }
}

优化规则包括:

  • 合并连续的 cursorMove 补丁
  • 丢弃无操作的光标移动(0,0)
  • 拼接相邻的样式补丁(避免多次 ANSI 转义)
  • 去重连续的相同超链接
  • 取消光标隐藏/显示配对

Sources: src/ink/optimizer.ts

智能缓存系统

带 TTL 的记忆化memoizeWithTTL 实现写通缓存模式,缓存过期时立即返回旧值并在后台异步刷新:

// src/utils/memoize.ts
export function memoizeWithTTL<Args extends unknown[], Result>(
  f: (...args: Args) => Result,
  cacheLifetimeMs: number = 5 * 60 * 1000,
): MemoizedFunction<Args, Result> {
  const cache = new Map<string, CacheEntry<Result>>()
  
  const memoized = (...args: Args): Result => {
    const cached = cache.get(key)
    
    if (!cached) {
      const value = f(...args)
      cache.set(key, { value, timestamp: now, refreshing: false })
      return value
    }
    
    // 缓存过期且未在刷新,后台异步刷新
    if (now - cached.timestamp > cacheLifetimeMs && !cached.refreshing) {
      cached.refreshing = true
      Promise.resolve()
        .then(() => {
          const newValue = f(...args)
          if (cache.get(key) === cached) {  // 身份守卫
            cache.set(key, { value: newValue, timestamp: Date.now(), refreshing: false })
          }
        })
      return cached.value  // 立即返回旧值
    }
  }
}

异步变体 memoizeWithTTLAsync 通过 inFlight Map 防止并发冷启动重复调用(如多个并发 aws sso login)。

Sources: src/utils/memoize.ts

文件读取缓存FileReadCache 基于文件修改时间自动失效,避免 FileEditTool 操作中的重复读取:

// src/utils/fileReadCache.ts
class FileReadCache {
  private cache = new Map<string, CachedFileData>()
  private readonly maxCacheSize = 1000
  
  readFile(filePath: string): { content: string; encoding: BufferEncoding } {
    const stats = fs.statSync(filePath)
    const cachedData = this.cache.get(filePath)
    
    // 缓存命中且未过期
    if (cachedData && cachedData.mtime === stats.mtimeMs) {
      return { content: cachedData.content, encoding: cachedData.encoding }
    }
    
    // 读取文件并更新缓存
    const encoding = detectFileEncoding(filePath)
    const content = fs.readFileSync(filePath, { encoding })
    this.cache.set(filePath, { content, encoding, mtime: stats.mtimeMs })
    
    // LRU 淘汰
    if (this.cache.size > this.maxCacheSize) {
      const firstKey = this.cache.keys().next().value
      if (firstKey) this.cache.delete(firstKey)
    }
  }
}

Sources: src/utils/fileReadCache.ts

文件状态缓存FileStateCache 使用 LRU 策略并基于字节大小淘汰,默认限制 100 个条目和 25MB 总大小:

// src/utils/fileStateCache.ts
export class FileStateCache {
  private cache: LRUCache<string, FileState>
  
  constructor(maxEntries: number, maxSizeBytes: number) {
    this.cache = new LRUCache<string, FileState>({
      max: maxEntries,
      maxSize: maxSizeBytes,
      sizeCalculation: value => Math.max(1, Buffer.byteLength(value.content)),
    })
  }
  
  // 路径归一化确保缓存命中率
  get(key: string): FileState | undefined {
    return this.cache.get(normalize(key))
  }
}

Sources: src/utils/fileStateCache.ts

上下文压缩

compact 服务通过智能压缩策略管理对话上下文,避免 token 超限和内存膨胀。压缩策略包括:

  • 微压缩:移除冗余消息,保留关键上下文
  • 自动压缩:在 token 预算接近上限时自动触发
  • 边界标记:通过 compact_boundary 消息标记压缩点,支持会话恢复
// src/services/compact/compact.ts
export async function compactConversation(
  messages: Message[],
  tools: Tool[],
  context: ToolUseContext,
): Promise<CompactResult> {
  // 分析上下文使用情况
  const analysis = await analyzeContext(messages, tools, context)
  
  // 执行压缩并生成摘要
  const compactedMessages = await runForkedAgent({
    messages,
    tools,
    systemPrompt: asSystemPrompt(COMPACT_PROMPT),
  })
  
  // 添加压缩边界标记
  const boundaryMessage = createCompactBoundaryMessage()
  return {
    messages: [boundaryMessage, ...compactedMessages],
    stats: analysis.stats,
  }
}

Sources: src/services/compact/compact.ts

性能优化最佳实践

启动性能优化清单

  1. 延迟非关键初始化:将 OpenTelemetry、1P 日志等重型依赖改为动态导入
  2. 并行预取:Keychain 读取、MDM 设置、API 预连接与主线程并行
  3. 懒加载 Schema:Zod Schema 使用 lazySchema 延迟构造
  4. 优化导入顺序:将副作用导入(如 startMdmRawRead)放在模块顶层

运行时性能优化清单

  1. 虚拟化长列表:超过 100 项的列表使用虚拟滚动
  2. 智能缓存:对昂贵计算使用带 TTL 的记忆化,对 I/O 操作使用基于版本的缓存
  3. 批量更新:合并连续的终端补丁,减少渲染开销
  4. 量化重新渲染:对高频事件(滚动、输入)使用量化阈值控制 React 更新频率

内存管理策略

  1. LRU 淘汰:所有缓存使用 LRU 策略,设置合理的条目数和大小限制
  2. 上下文压缩:定期压缩对话历史,避免无限增长
  3. 弱引用缓存:对可重建的派生数据使用 WeakMap
  4. 及时清理:通过 cleanupRegistry 注册退出时的清理逻辑

性能监控与分析

# 启用详细启动性能分析
CLAUDE_CODE_PROFILE_STARTUP=1 claude

# 启用查询性能分析
CLAUDE_CODE_PROFILE_QUERY=1 claude

# 查看性能报告
cat ~/.claude/startup-perf/*.txt

Sources: src/utils/startupProfiler.ts, src/utils/queryProfiler.ts

性能优化效果

通过上述优化策略,Claude Code 实现了以下性能目标:

  • 启动时间:从 CLI 入口到 REPL 就绪约 200-300ms(含 100-200ms 的网络预连接)
  • 首 Token 延迟:通过预连接和并行初始化,TTFT 降低 30-50%
  • 内存占用:虚拟滚动将长对话内存占用从 O(n) 降至 O(viewport + overscan)
  • 渲染性能:优化器将终端补丁数量减少 40-60%,FPS 稳定在 60

性能优化是持续演进的过程,项目通过完善的性能分析体系和量化指标,确保每次优化都有据可依且效果可测量。开发者可根据实际场景,灵活应用这些策略到自己的项目中。