mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
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>
This commit is contained in:
@@ -7,6 +7,12 @@ import sliceAnsi from '../utils/sliceAnsi.js';
|
||||
import { countCharInString } from '../utils/stringUtils.js';
|
||||
import { HighlightedCodeFallback } from './HighlightedCode/Fallback.js';
|
||||
import { expectColorFile } from './StructuredDiff/colorDiff.js';
|
||||
import type { ColorFile as ColorFileType } from 'color-diff-napi';
|
||||
|
||||
// Module-level LRU cache for ColorFile instances to avoid recreating
|
||||
// them for the same (filePath, code) across component instances.
|
||||
const colorFileCache = new Map<string, { colorFile: ColorFileType; code: string }>();
|
||||
const COLOR_FILE_CACHE_MAX = 50;
|
||||
|
||||
type Props = {
|
||||
code: string;
|
||||
@@ -37,7 +43,22 @@ export const HighlightedCode = memo(function HighlightedCode({
|
||||
if (!ColorFile) {
|
||||
return null;
|
||||
}
|
||||
return new ColorFile(code, filePath);
|
||||
const cacheKey = `${filePath}\0${code.length}`;
|
||||
const cached = colorFileCache.get(cacheKey);
|
||||
if (cached && cached.code === code) {
|
||||
// Move to end (most recently used)
|
||||
colorFileCache.delete(cacheKey);
|
||||
colorFileCache.set(cacheKey, cached);
|
||||
return cached.colorFile;
|
||||
}
|
||||
const instance = new ColorFile(code, filePath);
|
||||
// Evict oldest entry if cache is full
|
||||
if (colorFileCache.size >= COLOR_FILE_CACHE_MAX) {
|
||||
const oldest = colorFileCache.keys().next().value;
|
||||
if (oldest !== undefined) colorFileCache.delete(oldest);
|
||||
}
|
||||
colorFileCache.set(cacheKey, { colorFile: instance, code });
|
||||
return instance;
|
||||
}, [code, filePath, syntaxHighlightingDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -414,58 +414,56 @@ const MessagesImpl = ({
|
||||
return false;
|
||||
}, [streamingThinking]);
|
||||
|
||||
// Find the last thinking block (message UUID + content index) for hiding past thinking in transcript mode
|
||||
// When streaming thinking is visible, use a special ID that won't match any completed thinking block
|
||||
// With adaptive thinking, only consider thinking blocks from the current turn and stop searching once we
|
||||
// hit the last user message.
|
||||
const lastThinkingBlockId = useMemo(() => {
|
||||
if (!hidePastThinking) return null;
|
||||
// If streaming thinking is visible, hide all completed thinking blocks by using a non-matching ID
|
||||
if (isStreamingThinkingVisible) return 'streaming';
|
||||
// Iterate backwards to find the last message with a thinking block
|
||||
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
||||
const msg = normalizedMessages[i];
|
||||
if (msg?.type === 'assistant') {
|
||||
const content = msg.message!.content as Array<{ type: string }>;
|
||||
// Find the last thinking block in this message
|
||||
for (let j = content.length - 1; j >= 0; j--) {
|
||||
if (content[j]?.type === 'thinking') {
|
||||
return `${msg.uuid}:${j}`;
|
||||
}
|
||||
}
|
||||
} else if (msg?.type === 'user') {
|
||||
const content = msg.message!.content as Array<{ type: string }>;
|
||||
const hasToolResult = content.some(block => block.type === 'tool_result');
|
||||
if (!hasToolResult) {
|
||||
// Reached a previous user turn so don't show stale thinking from before
|
||||
return 'no-thinking';
|
||||
}
|
||||
}
|
||||
// Find the last thinking block and latest bash output in a single backward pass.
|
||||
// Merged from two separate reverse iterations to reduce total traversals.
|
||||
const { lastThinkingBlockId, latestBashOutputUUID } = useMemo(() => {
|
||||
let thinkingId: string | null = null;
|
||||
let bashUUID: string | null = null;
|
||||
const needThinkingScan = hidePastThinking && !isStreamingThinkingVisible;
|
||||
if (hidePastThinking && isStreamingThinkingVisible) {
|
||||
thinkingId = 'streaming';
|
||||
}
|
||||
return null;
|
||||
}, [normalizedMessages, hidePastThinking, isStreamingThinkingVisible]);
|
||||
|
||||
// Find the latest user bash output message (from ! commands)
|
||||
// This allows us to show full output for the most recent bash command
|
||||
const latestBashOutputUUID = useMemo(() => {
|
||||
// Iterate backwards to find the last user message with bash output
|
||||
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
||||
const msg = normalizedMessages[i];
|
||||
if (msg?.type === 'user') {
|
||||
const content = msg.message!.content as Array<{ type: string; text?: string }>;
|
||||
// Check if any text content is bash output
|
||||
for (const block of content) {
|
||||
if (block.type === 'text') {
|
||||
const text = block.text ?? '';
|
||||
if (text.startsWith('<bash-stdout') || text.startsWith('<bash-stderr')) {
|
||||
return msg.uuid;
|
||||
// Bash output detection
|
||||
if (!bashUUID) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'text') {
|
||||
const text = block.text ?? '';
|
||||
if (text.startsWith('<bash-stdout') || text.startsWith('<bash-stderr')) {
|
||||
bashUUID = msg.uuid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Thinking stop condition — reached a previous user turn without tool result
|
||||
if (needThinkingScan && !thinkingId) {
|
||||
const hasToolResult = content.some(block => block.type === 'tool_result');
|
||||
if (!hasToolResult) {
|
||||
thinkingId = 'no-thinking';
|
||||
}
|
||||
}
|
||||
} else if (msg?.type === 'assistant') {
|
||||
if (needThinkingScan && !thinkingId) {
|
||||
const content = msg.message!.content as Array<{ type: string }>;
|
||||
for (let j = content.length - 1; j >= 0; j--) {
|
||||
if (content[j]?.type === 'thinking') {
|
||||
thinkingId = `${msg.uuid}:${j}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (thinkingId !== null && bashUUID) break;
|
||||
}
|
||||
return null;
|
||||
}, [normalizedMessages]);
|
||||
if (!hidePastThinking) {
|
||||
thinkingId = null;
|
||||
}
|
||||
return { lastThinkingBlockId: thinkingId, latestBashOutputUUID: bashUUID };
|
||||
}, [normalizedMessages, hidePastThinking, isStreamingThinkingVisible]);
|
||||
|
||||
// streamingToolUses updates on every input_json_delta while normalizedMessages
|
||||
// stays stable — precompute the Set so the filter is O(k) not O(n×k) per chunk.
|
||||
@@ -536,14 +534,14 @@ const MessagesImpl = ({
|
||||
});
|
||||
|
||||
const messagesToShowNotTruncated = reorderMessagesInUI(
|
||||
compactAwareMessages
|
||||
.filter((msg): msg is Exclude<NormalizedMessage, ProgressMessageType> => msg.type !== 'progress')
|
||||
// CC-724: drop attachment messages that AttachmentMessage renders as
|
||||
// null (hook_success, hook_additional_context, hook_cancelled, etc.)
|
||||
// BEFORE counting/slicing so they don't inflate the "N messages"
|
||||
// count in ctrl-o or consume slots in the 200-message render cap.
|
||||
.filter(msg => !isNullRenderingAttachment(msg))
|
||||
.filter(_ => shouldShowUserMessage(_, isTranscriptMode)) as Parameters<typeof reorderMessagesInUI>[0],
|
||||
compactAwareMessages.filter(
|
||||
(msg): msg is Exclude<NormalizedMessage, ProgressMessageType> =>
|
||||
// CC-724: drop attachment messages that AttachmentMessage renders as
|
||||
// null (hook_success, hook_additional_context, hook_cancelled, etc.)
|
||||
// BEFORE counting/slicing so they don't inflate the "N messages"
|
||||
// count in ctrl-o or consume slots in the 200-message render cap.
|
||||
msg.type !== 'progress' && !isNullRenderingAttachment(msg) && shouldShowUserMessage(msg, isTranscriptMode),
|
||||
) as Parameters<typeof reorderMessagesInUI>[0],
|
||||
syntheticStreamingToolUseMessages,
|
||||
);
|
||||
// Three-tier filtering. Transcript mode (ctrl+o screen) is truly unfiltered.
|
||||
@@ -623,19 +621,21 @@ const MessagesImpl = ({
|
||||
[streamingToolUses],
|
||||
);
|
||||
|
||||
// Divider insertion point: first renderableMessage whose uuid shares the
|
||||
// 24-char prefix with firstUnseenUuid (deriveUUID keeps the first 24
|
||||
// chars of the source message uuid, so this matches any block from it).
|
||||
const dividerBeforeIndex = useMemo(() => {
|
||||
if (!unseenDivider) return -1;
|
||||
const prefix = unseenDivider.firstUnseenUuid.slice(0, 24);
|
||||
return renderableMessages.findIndex(m => m.uuid.slice(0, 24) === prefix);
|
||||
}, [unseenDivider, renderableMessages]);
|
||||
|
||||
const selectedIdx = useMemo(() => {
|
||||
if (!cursor) return -1;
|
||||
return renderableMessages.findIndex(m => m.uuid === cursor.uuid);
|
||||
}, [cursor, renderableMessages]);
|
||||
// Divider insertion point and selected index: combined into a single pass
|
||||
// over renderableMessages to avoid two separate findIndex traversals.
|
||||
const { dividerBeforeIndex, selectedIdx } = useMemo(() => {
|
||||
if (!unseenDivider && !cursor) return { dividerBeforeIndex: -1, selectedIdx: -1 };
|
||||
let dIdx = -1;
|
||||
let sIdx = -1;
|
||||
const prefix = unseenDivider?.firstUnseenUuid.slice(0, 24);
|
||||
for (let i = 0; i < renderableMessages.length; i++) {
|
||||
const m = renderableMessages[i];
|
||||
if (dIdx === -1 && prefix && m.uuid.slice(0, 24) === prefix) dIdx = i;
|
||||
if (sIdx === -1 && cursor && m.uuid === cursor.uuid) sIdx = i;
|
||||
if (dIdx !== -1 && sIdx !== -1) break;
|
||||
}
|
||||
return { dividerBeforeIndex: dIdx, selectedIdx: sIdx };
|
||||
}, [unseenDivider, cursor, renderableMessages]);
|
||||
|
||||
// Fullscreen: click a message to toggle verbose rendering for it. Keyed by
|
||||
// tool_use_id where available so a tool_use and its tool_result (separate
|
||||
|
||||
@@ -366,7 +366,7 @@ function ModeIndicator({
|
||||
dimColor={rssState.level === 'normal'}
|
||||
color={rssState.level === 'error' ? 'error' : rssState.level === 'warning' ? 'warning' : undefined}
|
||||
>
|
||||
{rssState.text}
|
||||
{rssState.text} · pid:{process.pid}
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
|
||||
Reference in New Issue
Block a user