Skip to main content
BCC
s23

Startup Performance

Streaming & Performance

从 3 秒到 300 毫秒

~300 lines of code9 tools懒加载 + 并行 prefetch + 快速路径 + profileCheckpoint
CLI 启动速度是用户留存的第一个门槛

The fastest code is code that doesn't run

Fast is a feature

[ Phase 5: 流式与性能 ] · 工具数: 9 · 代码量: ~400 行


前置知识

  • 需要完成: s22 [工具并行执行]

你将学到

  • 快速路径(fast path):--version 不加载 AI 模块
  • 动态 import() 懒加载重量级依赖
  • 并行预加载(prefetch)I/O 操作
  • 性能分析与优化方法论

用户执行 mycli --version,期望立即看到版本号。但我们的 CLI 需要:

启动加载链(全部串行):

  ┌─ 1. 加载 TypeScript 运行时 ───┐
  │        (100ms)          │
  └──────────┬──────────────┘
             ↓
  ┌─ 2. 导入 React + Ink ────────┐
  │        (800ms)          │
  └──────────┬──────────────┘
             ↓
  ┌─ 3. 创建 Anthropic Client ───┐
  │        (200ms)          │
  └──────────┬──────────────┘
             ↓
  ┌─ 4. 加载工具注册表 ─────────┐
  │        (300ms)          │
  └──────────┬──────────────┘
             ↓
  ┌─ 5. 读取 RULES.md ──────┐
  │        (150ms)          │
  └──────────┬──────────────┘
             ↓
  ┌─ 6. 检测项目根目录 ─────┐
  │        (100ms)          │
  └─────────────────────────┘

  总计: ~1650ms (用户盯着空白屏幕)

所有这些都在 import 阶段串行发生。结果?一个 --version 需要 2-3 秒。

CLI 启动速度是用户留存的第一个门槛。 每慢 100ms,用户就多一分烦躁。

设计决策

快速路径 (Fast Path)

--version--help 不需要任何业务模块。在 import 任何东西之前就处理它们:

// cli.tsx — 最顶部
const args = process.argv.slice(2);

if (args.includes("--version")) {
  console.log("mycli 0.23.0");
  process.exit(0);  // 不到 10ms 退出
}

懒加载 (Lazy Loading)

重量级模块用动态 import() 延迟到真正需要时:

// 不是顶层 import
// import React from "react";
// import { render } from "ink";

// 而是在需要时动态加载
const [React, { render }, { ReplScreen }] = await Promise.all([
  import("react"),
  import("ink"),
  import("./components/repl-screen.js"),
]);

Promise.all 让三个模块的加载并行,而不是串行。

并行预取 (Parallel Prefetch)

启动时有多个独立的 I/O 操作,它们之间没有依赖关系:

串行执行(4 个 I/O 排队):

  detectProjectRoot    loadRules      validateApiKey      detectGit
       200ms             100ms            50ms              100ms
  ├─────────────┼─────────────┼─────────────┼─────────────┤
  0ms          200ms         300ms         350ms          450ms

  总计: 450ms

并行执行(Promise.all):

  detectProjectRoot (200ms)
       ════════════════════════════════════════
  loadRules (100ms)
       ════════════════
  validateApiKey (50ms)
       ═════════
  detectGit (100ms)
       ════════════════
  ├──────────────────────────────────────────────────────────┤
  0ms                                                      200ms

  总计: max(200, 100, 50, 100) = 200ms  (节省 56%)
export function startPrefetch(cwd: string) {
  return Promise.all([
    detectProjectRoot(cwd),
    loadRulesFile(cwd),
    validateApiKey(),
    detectGit(cwd),
  ]);
}

关键:在动态 import 主程序之前就启动 prefetch,让 I/O 和模块加载并行:

优化后的启动时间线(I/O 与 CPU 并行):

  0ms      50ms     100ms    150ms    200ms    250ms    300ms
   ├────────┼────────┼────────┼────────┼────────┼────────┤
   │                                                     │
   │ ═══════════════════════════════════════              │  prefetch (I/O)
   │          200ms                                      │
   │     ════════════════════════════════════════════════ │  dynamic import
   │          (React, Ink, ReplScreen)  300ms             │
   │                                                     │
   └─────────────────────────────────────────────────────┘
                                                     ↓
                                              300ms 时两者都完成
                                                     ↓
                                                  render()
                                                     ↓
                                                  界面显示

对比: 串行启动 500ms → 并行启动 300ms (节省 40%)

性能打点 (Profile Checkpoint)

const startTime = performance.now();

export function profileCheckpoint(label: string) {
  checkpoints.push({ label, elapsed: performance.now() - startTime });
}

设置 CLI_PROFILE=1 查看启动报告:

⏱  Startup Profile:
────────────────────────────────────────
  cli_entry                    0.1ms  (+0.1ms)
  fast_path_done               0.3ms  (+0.2ms)
  prefetch_fired               1.2ms  (+0.9ms)
  main_start                   1.5ms  (+0.3ms)
  imports_done               145.0ms  (+143.5ms)
  prefetch_resolved          146.2ms  (+1.2ms)
  first_render               152.0ms  (+5.8ms)
────────────────────────────────────────
  Total: 152.0ms

实现

CLI 入口:分层启动

// 1. 打点
profileCheckpoint("cli_entry");

// 2. 快速路径
if (args.includes("--version")) { ... process.exit(0); }

// 3. 启动 prefetch
const prefetch = startPrefetch(process.cwd());

// 4. 动态加载主程序
async function main() {
  const [React, { render }, { ReplScreen }] = await Promise.all([...]);
  await prefetch;
  render(React.createElement(ReplScreen));
}

main();

Prefetch 模块

let prefetchPromise: Promise<PrefetchResults> | null = null;

export function startPrefetch(cwd: string) {
  if (prefetchPromise) return prefetchPromise; // 幂等
  prefetchPromise = Promise.all([
    detectProjectRoot(cwd),
    loadRulesFile(cwd),
    validateApiKey(),
    detectGit(cwd),
  ]);
  return prefetchPromise;
}

延迟预取 (Deferred Prefetch)

有些操作可以在首次渲染之后再做(不影响用户看到界面):

首次渲染前(必须):
  - detectProjectRoot
  - loadRules
  - validateApiKey

首次渲染后(延迟):
  - 检查更新
  - 加载用户偏好
  - 预热 API 连接

运行验证

cd agents/s23-startup-perf

# 快速路径测试
npm run dev -- --version  # 应该立即输出版本号

# 性能分析
npm run dev -- --profile  # 启动后查看 stderr 的性能报告

对照 Claude Code

维度教学版 (s23)Claude Code
快速路径--version / --help--version + 10+ 子命令各自 lazy import
模块加载3 个动态 import几十个按需 import,分子命令路由
预取数量4 个~15 个(keychain、MCP URLs、AWS/GCP credentials、配额...)
延迟预取未实现startDeferredPrefetches() 在首次渲染后触发
安全门控部分预取需等用户接受 trust 后才执行
性能度量performance.now() 打点profileCheckpoint + telemetry 上报
缓存resolveFastModeStatusFromCache — 先用缓存,后台刷新

Claude Code 的分层启动

cli.tsx (bootstrap)
  ├── --version → 直接退出
  ├── --dump-system-prompt → 仅 import prompt 模块
  ├── daemon-worker → 仅 import daemon 模块
  └── 完整 REPL → import main.tsx
        ├── startMdmRawRead() — 并行子进程
        ├── startKeychainPrefetch() — 并行 keychain
        ├── ... 模块初始化 ...
        └── startDeferredPrefetches() — 渲染后延迟

每个子命令只加载自己需要的模块。main.tsx 是最重的路径,但也不是所有代码都在顶层 import

深入思考

  1. 动态 import 的代价:每个 import() 有一个微小的开销(创建 Promise、解析模块)。对于频繁调用的热路径不合适。启动时的一次性加载是最佳场景。
  2. prefetch 的幂等性startPrefetch 用模块级变量缓存 Promise。多次调用不会重复执行。这是一个常见的"懒初始化"模式。
  3. 测量驱动优化:没有 profileCheckpoint,你不知道时间花在哪里。先测量,再优化——否则可能在不重要的地方白费力气。

练习

  1. 在你的机器上实际测量:time npm run dev -- --version,然后对比 time npm run dev(完整启动)
  2. 实现 startDeferredPrefetches():在首次渲染 200ms 后执行一些低优先级初始化
  3. 添加一个缓存层:把 detectProjectRoot 的结果缓存到 /tmp,下次启动直接读取

Phase 5 总结

恭喜!完成 s20-s23,你的 Agent 已经有了流式输出和生产级性能

  • ✅ 基础 Streaming(s20)
  • ✅ Streaming 进阶(s21)
  • ✅ 工具并行执行(s22)
  • ✅ 启动性能优化(s23)

下一个 Phase 将解决 Agent 最重要的工程挑战——上下文管理。当对话越来越长,怎么避免 token 爆炸?s24 上下文压缩 开始。

下一课预告

Agent 对话 20 轮后,messages 数组可能达到 50000+ tokens。下一课 s24 上下文压缩 将实现对话历史的自动压缩,让 Agent 能在有限上下文窗口内持续工作。