Files
claude-code/docs/memory-peak-analysis.md
claude-code-best ef10ad2839 fix: 优化内存峰值与 CPU 性能,降低 100-300MB 内存占用
- claude.ts: 流式字符串拼接从 O(n²) += 改为数组累积 join,消除 4 处热点
- Messages.tsx: 合并 3 组独立遍历为单次 pass(thinking/bash 查找、3-filter 链、divider/selectedIdx)
- HighlightedCode.tsx: ColorFile 实例添加模块级 LRU 缓存(50 条),避免重复创建
- screen.ts: StylePool 衍生缓存添加 1000 条上限淘汰,防止无界增长
- CompanionSprite.tsx: TICK_MS 从 500ms 提升至 1000ms,减少 setState 频率
- connection.ts: MCP stderr 缓冲从 64MB 降至 8MB
- stringUtils.ts: MAX_STRING_LENGTH 从 32MB 降至 2MB
- sessionStorage.ts: Transcript 写入队列添加 1000 条上限
- query.ts: spread 改 concat 减少一次数组拷贝
- PromptInputFooterLeftSide.tsx: 显示进程 pid 便于调试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 00:45:03 +08:00

12 KiB
Raw Blame History

内存与性能峰值分析报告

进程bun物理内存峰值 700 MB+,最差场景可达 1.8 GB 日期2026-05-017 轮排查 + 验证,已压缩) 范围:内存峰值 + CPU 热点 + React 循环 effect

数据收集

  • 典型场景 RSS 682 MB基线 JSC heap 300-400 MB
  • Bun mimalloc 不归还内存页JSC 页管理只增不减(架构级)
  • 已有每秒 Bun.gc() 定时器(cli/print.ts:554-558),非强制模式
  • 前置修复commit ab0bbbc4scrollback 限 500、contentReplacementState 清理等

内存问题(按峰值影响排序)

# 来源 峰值 位置 验证状态
1 消息数组 6-7x 拷贝 120-320 MB query.ts:477,491,1135,1745,1878 已验证,比原估 4x 更严重
2 Messages.tsx 转换管线 24-25x 遍历 100-270 MB Messages.tsx:405-619 已验证,比原估 3-4x 严重得多
3 语法高亮 ColorFile 无 LRU 50-100 MB HighlightedCode.tsx:32-41 已验证,每个组件实例新建 ColorFile
4 BashTool 输出缓冲32MB/命令) 30-330 MB stringUtils.ts:88 (2**25 = 32MB) 已验证
5 Compact 峰值(老+新共存) 20-80 MB compact/compact.ts:393-547 已验证old messages + summary + fileState 同时在内存
6 MCP stderr 缓冲64MB/server 1-640 MB mcp-client/src/connection.ts:117 已验证,默认 64MB
7 MCP Tool Schema 双重存储 ~40 MB services/mcp/useManageMCPConnections.ts:258 + AppStateStore.ts:175 已验证LRU cache + AppState 各一份
8 Transcript 写入队列(无上限) 5-50 MB utils/sessionStorage.ts:559-615 已验证,无 size check100ms drain
9 lastAPIRequestMessages 常驻 30-50 MB bootstrap/state.ts:118 已验证,仅 ant 用户、/clear 时清空
10 流式字符串拼接(+= O(n²) 2-20 MB claude.ts:2147-2228 已验证4 处 += 拼接
11 Session 恢复全量加载 50-200 MB utils/sessionStorage.ts:3475-3582 已验证,大文件有优化但中小文件仍全量
12 Ink StylePool 无界增长 10-50+ MB @ant/ink/src/core/screen.ts:112-180 已验证4 个无界 Map + 无界数组
13 Dev mode 50+ features 50-100 MB scripts/dev.ts:29-34 未验证dev only
14 AppState 不可变更新抖动 5-50 MB store.ts:20-26 已验证,每次更新创建新对象
15 OpenTelemetry 多版本 ~30 MB 依赖树 未验证(低优先级)
16 Perfetto tracing 100K events ~30 MB perfettoTracing.ts:99 未验证(低优先级)
17 Prompt Cache 规范化 5-15 MB claude.ts:3180-3329 未验证(低优先级)
18 GrepTool 全量 stat+sort ~10 MB GrepTool.ts:523-557 未验证(低优先级)
19 mimalloc + JSC 不归还内存 RSS 持续高位 Bun 运行时 架构确认

验证详情

#1 消息数组 6-7x 拷贝P0

原始估计 4x实际验证发现更多拷贝点

位置 操作 拷贝类型
query.ts:477 + utils/messages.ts:4830 getMessagesAfterCompactBoundaryslice() + [...result] 浅拷贝 ×2
query.ts:491 applyToolResultBudgetmessages.map() 浅拷贝 ×1
query.ts:1135 executePostSamplingHooks([...messages, ...assistant]) spread 合并 ×1
query.ts:1745 getAttachmentMessages(null, ctx, null, cmds, [...msgs, ...asst, ...results]) spread 合并 ×1
query.ts:1878 State 更新 { messages: [...msgs, ...asst, ...results] } spread 合并 ×1
query.ts:897 clonedContent ??= [...contentArr] 条件性拷贝 ×1

总计每轮查询循环 6-7 次数组浅拷贝。单次拷贝开销小(指针数组),但累积峰值叠加时占用大量临时内存。

#2 Messages.tsx 24-25 次遍历P1

原始估计 3-4 次,实际有 10 个独立处理阶段:

  1. normalizedMessages (行 405) — normalizeMessages + filter = 2 次
  2. lastThinkingBlockId (行 421-446) — 反向遍历 = 1 次
  3. latestBashOutputUUID (行 450-468) — 反向遍历 = 1 次
  4. normalizedToolUseIDs (行 472) — getToolUseIDs = 1 次
  5. streamingToolUsesWithoutInProgress (行 474-480) — filter = 1 次
  6. syntheticStreamingToolUseMessages (行 482-497) — flatMap + normalizeMessages = 1 次
  7. 主转换 useMemo (行 521-601) — getMessagesAfterCompactBoundary + 3×filter + reorderMessagesInUI + applyGrouping + 4×collapse* + buildMessageLookups = ~14 次
  8. renderableMessages (行 604-619) — slice = 1 次
  9. dividerBeforeIndex (行 629-633) — findIndex = 1 次
  10. selectedIdx (行 635-638) — findIndex = 1 次

总计 ~24 次遍历,主转换 useMemo 单独贡献 14 次。

#3 ColorFile 无 LRUP1

  • HighlightedCode.tsx:32-41:每次 useMemo 创建新 ColorFile(code, filePath) 实例
  • color-diff-napi 内部有全局 hlLineCacheMap上限 2048 条目)缓存 AST但不缓存渲染结果
  • 无跨实例复用,大量代码块场景下每个组件持有一份完整 code 字符串

#4 BashTool 输出缓冲P2

  • stringUtils.ts:88const MAX_STRING_LENGTH = 2 ** 25 = 32 MB(非 33MB
  • 单条 Bash 命令输出可占 32MB 后才触发截断

#5 Compact 峰值

  • compact/compact.ts:407:先 tokenCountWithEstimation(messages) 遍历全量
  • 整个 compact 过程 messages 数组不释放
  • 额外创建 preCompactReadFileStatepostCompactFileAttachmentsasyncAgentAttachments
  • 峰值 = old messages + API summary response + file state + attachments

#6 MCP stderr 缓冲

  • mcp-client/src/connection.ts:117maxSize = 64 * 1024 * 102464MB 默认值)
  • 每个 MCP server 连接独立缓冲10 个 server = 640MB 理论上限

#7 MCP Tool Schema 双重存储

  • services/mcp/useManageMCPConnections.ts:258:更新时 [...reject(mcp.tools, ...), ...tools] 创建新数组
  • 存储位置:fetchToolsForClient LRU20 条目)+ AppState.mcp.tools 数组
  • 20 servers × ~50 tools × ~2KB/tool ≈ 2MB 重复

#8 Transcript 写入队列

  • utils/sessionStorage.ts:561-564writeQueues = new Map<string, Array<{entry, resolve}>>() 无大小限制
  • 每 100ms drainFLUSH_INTERVAL_MS = 100),高频写入时条目堆积

#9 lastAPIRequestMessages

  • bootstrap/state.ts:118:声明为模块级变量
  • ant 用户设置(log.ts:350),非 ant 用户直接 null
  • /clear 时通过 clear/conversation.ts:155 清空

#10 流式字符串拼接

  • claude.ts 中 4 处 += 操作:
    • 行 2147-2148connector_text += delta.connector_text
    • 行 2178contentBlock.input += delta.partial_json
    • 行 2192contentBlock.text += delta.text
    • 行 2227-2228contentBlock.thinking += delta.thinking
  • 长流式响应时产生 O(n²) 内存分配

#11 Session 恢复

  • 大文件(> SKIP_PRECOMPACT_THRESHOLD):使用 readTranscriptForLoad() 只加载 post-boundary 内容,有优化
  • 中小文件(< thresholdreadFile(filePath) 全量读入
  • 优化已部分到位,但阈值以下的文件仍全量加载

#12 Ink StylePool

  • screen.ts:112-180StylePool 类含 4 个无界 Map/Array
    • ids: Map<string, number> — style key → id
    • styles: AnsiCode[][] — 无界数组
    • transitionCache: Map<number, string>
    • inverseCache: Map<number, number>
    • currentMatchCache: Map<number, number>
  • intern() 只 push 不淘汰

#14 AppState 不可变更新

  • store.ts:20-26setState 要求返回新对象,Object.is 比较后通知
  • useManageMCPConnections.ts 每次 MCP 更新 spread 整个 prevState

CPU 与渲染热点(第 6 轮探索 + 第 7 轮验证)

已确认

# 问题 影响 位置
C2 Ink 每次 React commit 触发 Yoga 布局(但 React ConcurrentRoot 自动批处理 setState5 个 setState → 1 次 commit → 1 次布局) ~1-3ms/次 commit reconciler.ts:279ink.tsx:323
C3 MessageRow 挂载成本 ~1.5ms(但 Markdown 解析仅占 1-7%,主因是 React/Yoga/Ink 管线开销 ~1.3ms 已有 SLIDE_STEP=25 + useDeferredValue 限速 useVirtualScroll.ts + Markdown.tsx
C4 布局偏移触发全屏 damage O(rows×cols) 全量 diff ink.tsx:655-661
C7 CompanionSprite TICK_MS 定时器500ms每秒 2 次 setState 高频 setState 触发渲染 buddy/CompanionSprite.tsx:15,136
C9 同步 fs 操作 阻塞主线程 projectOnboardingState.ts:20

已否认

  • C1 useInboxPoller 状态循环 — 验证确认useEffect 是收敛的(移除消息 → count 减少 → 稳定poll 通过 store.getState() 读取不触发 React 依赖1 秒轮询是正常 I/O 模式无循环
  • Markdown 是 CPU 热点 — marked.lexer 对典型消息仅 0.01-0.1ms,已有 tokenCache LRU-500缓存命中 0.0003ms99.6% 降速)+ hasMarkdownSyntax 快速路径(跳过 30-40% 消息)
  • Yoga 无增量布局 — 实测增量更新高效1000 节点树改 1 叶子 → 仅 2 次 measure其余走缓存
  • Ink Yoga 2^depth 问题 — 实测 100 节点深链 = 11.7x 访问(线性增长,非指数级)

已确认的优化措施(已有)

  • React ConcurrentRoot 自动批处理 setState多个 setState → 1 次 commit
  • Ink 帧率限制 16msthrottle 仅限终端输出Yoga 布局无 throttle 但被 React batching 保护)
  • 虚拟滚动 overscan 80 + MAX_MOUNTED_ITEMS 300 + SLIDE_STEP=25 + useDeferredValue
  • Markdown tokenCache LRU-500 + hasMarkdownSyntax 快速路径 + StreamingMarkdown 增量解析
  • Yoga 增量缓存dirty propagation + measure 结果缓存)
  • 双缓冲 + damage tracking + 字符池复用
  • Pool 5 分钟周期重置

已否认

  • VSZ 516 GB 是虚拟地址映射非物理内存
  • RSS 波动是正常 GC 行为
  • useSkillsChange / useSettingsChange 订阅泄漏 — 验证为正确的 React cleanup 模式
  • Zod Schema 开销 — 仅 ~200-650KB已有 lazySchema + WeakMap 缓存
  • Ink ClockContext 16ms 定时器 — 影响 CPU 不影响内存

结论

内存根因:消息数组多重拷贝 + JSC/mimalloc 不归还内存。典型 700 MB最差 1.8 GB。

CPU 根因useInboxPoller 每秒轮询触发 React commit → 全量 Yoga 布局 → 全屏 Ink diff 的完整管线。Markdown 渲染(~1.5ms/行)在批量挂载新消息时造成 ~290ms 卡顿。这两者叠加:轮询导致的周期性 commit 与消息挂载的 CPU 密集操作互相放大。

建议

P0消息数组拷贝降 100-200 MB 内存)

  1. query.ts:491 — applyToolResultBudget 按需拷贝
  2. query.ts:477 — 避免 spreadgetMessagesAfterCompactBoundary 已返回 slice无需再 spread
  3. query.ts:1878 — 追加而非重建(用 push 替代 [...prev, ...new]
  4. query.ts:1135,1745 — read-only 场景传引用而非 spread

P1渲染管线降 50-150 MB + 降低 CPU

  1. Messages.tsx:521-601 — 合并主转换 useMemo 中的 14 次遍历为单次 pass
  2. HighlightedCode.tsx:32-41 — ColorFile 实例级 LRU50 条)或 WeakMap 跨实例复用
  3. buddy/CompanionSprite.tsx:15 — TICK_MS 从 500ms 评估能否提升至 1000ms+

P2Ink 渲染层(降低 CPU 开销)

  1. ink.tsx:655-661 — 布局偏移时尝试增量 damage 而非全屏 {x:0,y:0,width:full,height:full}

P3内存 + 低优先级

  1. lastAPIRequestMessages — 非 debug 清空
  2. claude.ts:2147-2228 — 4 处流式 += 改数组累积后 join('')
  3. mcp-client/connection.ts:117 — stderr 缓冲从 64MB 降至 8MB
  4. Session 恢复中小文件也使用流式解析
  5. BashTool MAX_STRING_LENGTH 从 32MB 降至 2MB
  6. MCP Tool Schema 消除双重存储(只保留 AppState 一份)
  7. Ink StylePool 加 LRU 淘汰(如 1000 条目上限)
  8. Transcript 写入队列加 maxQueueSize 限制
  9. OpenTelemetry 统一版本
  10. AppState 无界集合加淘汰策略
  11. GrepTool 先 limit 再 stat+sort
  12. 评估 Bun.gc(true) 强制 GC