第25章:性能优化——毫秒必争

为什么性能很重要?

对于一个 CLI 工具来说,速度就是用户体验。如果输入 claude 后要等 3 秒才出现界面,用户会觉得这个工具"很慢"。如果 AI 每次回复都要等 30 秒,用户会失去耐心。

Claude Code 的工程师们在每个环节都追求更快的速度。让我们看看他们用了哪些技巧。

优化一:并行预加载

还记得第 4 章介绍的启动过程吗?


// 在导入模块之前就开始耗时操作
startMdmRawRead()        // 读取系统策略 ~20ms
startKeychainPrefetch()  // 读取密钥 ~60ms

// 然后才开始导入模块
import { App } from "./components/App"     // ~100ms
import { createStore } from "./state"       // ~30ms

这两个操作和模块导入是完全独立的——它们之间没有依赖。所以可以同时进行:


不优化时(串行):
  密钥读取(60ms) → 策略读取(20ms) → 模块导入(130ms) = 210ms

优化后(并行):
  密钥读取(60ms) ──────────┐
  策略读取(20ms) ───┐      │
  模块导入(130ms)──────────┘ = 130ms

节省了 80ms——几乎减少了 40% 的启动时间。

这个优化的关键洞察是:不要等到需要结果的时候才开始工作,而是尽早开始。

优化二:提示缓存

这是最重要的优化之一,节省了约 90% 的 token 费用

每次 API 请求都包含系统提示词(约 5,000 tokens)和工具定义(约 8,000 tokens)。这 13,000 tokens 每次都一样,但每次都要付费。

通过提示缓存:


第一次请求:
  系统提示词(5,000 tokens) → 创建缓存 → 费用 = 5,000 × 正常价格

后续请求:
  系统提示词 → 命中缓存 → 费用 = 5,000 × 0.1 × 正常价格 (90% 折扣)

为什么工具按名字排序?


// 排序前:每次工具顺序可能不同
tools: [FileRead, Bash, Grep, ...]     // 请求 A
tools: [Bash, FileRead, Grep, ...]     // 请求 B → 缓存失效!

// 排序后:顺序始终一致
tools: [Bash, FileRead, Grep, ...]     // 请求 A
tools: [Bash, FileRead, Grep, ...]     // 请求 B → 缓存命中!

如果工具顺序变了,系统提示词的文本就变了,缓存就失效了。按名字排序确保每次顺序一致。

这种"为了缓存而保持稳定性"的思维在性能优化中非常常见。

优化三:惰性精确计算

Token 计数是一个频繁的操作。精确计算需要 API 调用(~100ms),但估算几乎不花时间:


function getTokenCount(text: string): number {
  const estimated = Math.ceil(text.length / 4)

  // 离上限还远?用估算
  if (estimated < MAX_TOKENS * 0.7) {
    return estimated
  }

  // 接近上限?精确计算
  return await exactTokenCount(text)
}

90% 的时间用估算(0ms),10% 的关键时刻才精确计算(100ms)。

这个策略的名字叫惰性计算(Lazy Evaluation)——只有在真正需要精确值的时候才做精确计算。

优化四:LRU 文件缓存

当 AI 在同一次对话中多次读取同一个文件时,不需要每次都从磁盘读:


const fileCache = new LRU<string, FileContent>(100)  // 最多缓存 100 个文件

async function readFile(path: string): Promise<FileContent> {
  // 缓存命中?直接返回
  const cached = fileCache.get(path)
  if (cached) return cached

  // 缓存未命中?从磁盘读取
  const content = await fs.readFile(path, "utf-8")
  fileCache.set(path, content)
  return content
}

LRU(Least Recently Used,最近最少使用)是一种缓存淘汰策略——当缓存满了,淘汰最久没用过的条目。


缓存(容量3): [C, B, A]  ← A 最久没用

读取 D → 缓存满了 → 淘汰 A
缓存: [D, C, B]

读取 B → B 已在缓存中 → 移到最前
缓存: [B, D, C]

优化五:推测性执行

当 AI 的回复还在流式传输时,Claude Code 已经可以开始准备:


时间线(不优化):
  AI 流式输出(3s) → 识别工具名 → 权限检查(200ms) → 执行工具(500ms)
  总计: 3.7s

时间线(推测性执行):
  AI 流式输出(3s) ─────────────────────┐
  AI 输出工具名后立刻开始权限检查(200ms)─┘
  → 执行工具(500ms)
  总计: 3.5s

当 AI 刚开始输出工具名(比如 "Bash")时,Claude Code 就开始权限检查了——不等 AI 输出完整的工具参数。这样权限检查和 AI 生成参数可以并行进行。

优化六:延迟加载

不是所有代码都在启动时需要:


// 不优化:启动时加载所有模块
import { VoiceModule } from "./voice"         // +50ms
import { BrowserModule } from "./browser"     // +30ms
import { REPLModule } from "./repl"           // +20ms

// 优化:只在需要时才加载
const VoiceModule = feature('VOICE_MODE')
  ? require('./voice')   // 只有开启语音功能才加载
  : null

const BrowserModule = feature('WEB_BROWSER')
  ? require('./browser') // 只有开启浏览器功能才加载
  : null

如果 90% 的用户不用语音功能,那 50ms 的加载时间就是浪费。延迟加载确保只加载用户实际需要的模块。

优化七:结果截断

工具返回的结果可能非常大。不加限制会导致两个问题:

  1. 1. token 浪费:大结果占用 AI 的上下文窗口
  2. 2. 网络延迟:大数据传输需要更多时间

const MAX_RESULT_CHARS = 500_000  // 50 万字符上限

if (result.length > MAX_RESULT_CHARS) {
  // 截断并保存完整版本到磁盘
  const previewSize = 1000
  return {
    preview: result.slice(0, previewSize),
    fullPath: saveToDisk(result),
    message: `结果太大(${result.length}字符),已保存到文件`
  }
}

优化八:性能剖析

你不能优化你不能测量的东西。Claude Code 有内置的性能测量:


profileCheckpoint('main_entry')
// ... 导入模块 ...
profileCheckpoint('imports_done')
// ... 初始化 ...
profileCheckpoint('init_done')
// ... 渲染 ...
profileCheckpoint('render_done')

// 输出: main_entry → imports(45ms) → init(30ms) → render(25ms) = 100ms

每个 checkpoint 记录当前时间,最后计算每个阶段的耗时。如果某个阶段突然变慢了,工程师能立刻定位到问题。

性能优化的哲学

从这些优化中,我们可以总结出几条通用的性能优化原则:

1. 并行一切可以并行的

如果两个操作互不依赖,就同时执行它们。

2. 缓存重复的计算

如果一个计算的输入没变,就重用上次的结果。

3. 延迟不必要的工作

如果现在不需要,就不要做。等需要的时候再做。

4. 估算 > 精确(在大多数情况下)

如果估算值已经足够做决定,就不需要精确计算。

5. 保持稳定性以利用缓存

排序、规范化等看似多余的步骤,可能是为了保持缓存的有效性。

6. 测量,不要猜测

用数据说话。不要凭感觉优化——先测量,找到真正的瓶颈,再优化。

本章小结

  • - 并行预加载:启动时提前发起耗时操作
  • - 提示缓存:节省 90% 的重复 token 费用
  • - 惰性精确计算:90% 用估算,10% 精确计算
  • - LRU 缓存:避免重复读取同一文件
  • - 推测性执行:提前开始后续步骤
  • - 延迟加载:只加载需要的模块
  • - 结果截断:避免大数据占用资源
  • - 性能剖析:用数据驱动优化决策

性能优化的"80/20 法则"

在性能优化中,有一个著名的经验法则:80% 的性能问题来自 20% 的代码。

这意味着你不需要优化每一行代码——你只需要找到那 20% 的"热点代码",优化它们就能获得 80% 的性能提升。

Claude Code 的优化策略完美体现了这一点:


优化投入 vs 收益:

提示缓存:
  投入:修改 API 调用方式(~100 行代码)
  收益:节省 90% token 费用
  → 高收益,值得做

工具排序:
  投入:加一行排序代码
  收益:保持缓存命中率
  → 极低投入,高收益

并行预加载:
  投入:调整代码顺序(~20 行)
  收益:启动快 65ms
  → 低投入,可观收益

相反,花一周时间优化一个只被调用一次的函数,即使性能提升 100 倍,对用户体验也没有感知——这就是"过度优化"。

记住:先测量,再优化。优化有回报的地方,忽略没回报的地方。

下一章,我们将探索 Claude Code 的记忆系统——跨越会话的智慧。


本书由 everettjf 使用 Claude Code 分析泄露源码编写 | 保留出处即可自由转载