perf: 表格渲染效率升级

This commit is contained in:
claude-code-best
2026-05-03 10:10:47 +08:00
parent 4ca7a4895a
commit 3a2b6dde7c

View File

@@ -65,20 +65,40 @@ function wrapText(text: string, width: number, options?: { hard?: boolean }): st
* 2. Distributing available space proportionally * 2. Distributing available space proportionally
* 3. Wrapping text within cells (no truncation) * 3. Wrapping text within cells (no truncation)
* 4. Properly aligning multi-line rows with borders * 4. Properly aligning multi-line rows with borders
*
* Performance: uses per-render caches (formatCache, plainTextCache, wrapCache)
* to avoid redundant formatCell/wrapText calls across the multiple passes
* (width calculation, row line counting, rendering). Wrapped in React.memo
* to skip re-renders when props are unchanged.
*/ */
export function MarkdownTable({ token, highlight, forceWidth }: Props): React.ReactNode { export const MarkdownTable = React.memo(function MarkdownTable({
token,
highlight,
forceWidth,
}: Props): React.ReactNode {
const [theme] = useTheme(); const [theme] = useTheme();
const { columns: actualTerminalWidth } = useTerminalSize(); const { columns: actualTerminalWidth } = useTerminalSize();
const terminalWidth = forceWidth ?? actualTerminalWidth; const terminalWidth = forceWidth ?? actualTerminalWidth;
// Format cell content to ANSI string // Per-render caches — Token[] references are stable within a single token
// prop (from LRU cache in Markdown.tsx), so reference equality is sufficient.
const formatCache = new Map<Token[] | undefined, string>();
const plainTextCache = new Map<Token[] | undefined, string>();
function formatCell(tokens: Token[] | undefined): string { function formatCell(tokens: Token[] | undefined): string {
return tokens?.map(_ => formatToken(_, theme, 0, null, null, highlight)).join('') ?? ''; const cached = formatCache.get(tokens);
if (cached !== undefined) return cached;
const result = tokens?.map(_ => formatToken(_, theme, 0, null, null, highlight)).join('') ?? '';
formatCache.set(tokens, result);
return result;
} }
// Get plain text (stripped of ANSI codes)
function getPlainText(tokens: Token[] | undefined): string { function getPlainText(tokens: Token[] | undefined): string {
return stripAnsi(formatCell(tokens)); const cached = plainTextCache.get(tokens);
if (cached !== undefined) return cached;
const result = stripAnsi(formatCell(tokens));
plainTextCache.set(tokens, result);
return result;
} }
// Get the longest word width in a cell (minimum width to avoid breaking words) // Get the longest word width in a cell (minimum width to avoid breaking words)
@@ -149,43 +169,39 @@ export function MarkdownTable({ token, highlight, forceWidth }: Props): React.Re
columnWidths = minWidths.map(w => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH)); columnWidths = minWidths.map(w => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH));
} }
// Step 4: Calculate max row lines to determine if vertical format is needed // Step 4: Single-pass cell preparation — wraps each cell once, caches results
function calculateMaxRowLines(): number { // for reuse by both row-line counting and rendering.
let maxLines = 1; const wrapCache = new Map<Token[] | undefined, string[]>();
// Check header
for (let i = 0; i < token.header.length; i++) { function getWrappedLines(tokens: Token[] | undefined, colIndex: number): string[] {
const content = formatCell(token.header[i]!.tokens); const cached = wrapCache.get(tokens);
const wrapped = wrapText(content, columnWidths[i]!, { if (cached !== undefined) return cached;
hard: needsHardWrap, const formatted = formatCell(tokens);
}); const lines = wrapText(formatted, columnWidths[colIndex]!, {
maxLines = Math.max(maxLines, wrapped.length); hard: needsHardWrap,
} });
// Check rows wrapCache.set(tokens, lines);
for (const row of token.rows) { return lines;
for (let i = 0; i < row.length; i++) { }
const content = formatCell(row[i]?.tokens);
const wrapped = wrapText(content, columnWidths[i]!, { // Step 5: Calculate max row lines using cached wrapped results
hard: needsHardWrap, let maxRowLines = 1;
}); for (let i = 0; i < token.header.length; i++) {
maxLines = Math.max(maxLines, wrapped.length); maxRowLines = Math.max(maxRowLines, getWrappedLines(token.header[i]!.tokens, i).length);
} }
} for (const row of token.rows) {
return maxLines; for (let i = 0; i < row.length; i++) {
maxRowLines = Math.max(maxRowLines, getWrappedLines(row[i]?.tokens, i).length);
}
} }
// Use vertical format if wrapping would make rows too tall
const maxRowLines = calculateMaxRowLines();
const useVerticalFormat = maxRowLines > MAX_ROW_LINES; const useVerticalFormat = maxRowLines > MAX_ROW_LINES;
// Render a single row with potential multi-line cells // Render a single row with potential multi-line cells
// Returns an array of strings, one per line of the row // Returns an array of strings, one per line of the row
function renderRowLines(cells: Array<{ tokens?: Token[] }>, isHeader: boolean): string[] { function renderRowLines(cells: Array<{ tokens?: Token[] }>, isHeader: boolean): string[] {
// Get wrapped lines for each cell (preserving ANSI formatting) // Reuse cached wrapped lines — no redundant formatCell/wrapText
const cellLines = cells.map((cell, colIndex) => { const cellLines = cells.map((cell, colIndex) => getWrappedLines(cell.tokens, colIndex));
const formattedText = formatCell(cell.tokens);
const width = columnWidths[colIndex]!;
return wrapText(formattedText, width, { hard: needsHardWrap });
});
// Find max number of lines in this row // Find max number of lines in this row
const maxLines = Math.max(...cellLines.map(lines => lines.length), 1); const maxLines = Math.max(...cellLines.map(lines => lines.length), 1);
@@ -231,6 +247,7 @@ export function MarkdownTable({ token, highlight, forceWidth }: Props): React.Re
} }
// Render vertical format (key-value pairs) for extra-narrow terminals // Render vertical format (key-value pairs) for extra-narrow terminals
// Uses formatCell cache; wrapping uses terminal-width params (not column widths)
function renderVerticalFormat(): string { function renderVerticalFormat(): string {
const lines: string[] = []; const lines: string[] = [];
const headers = token.header.map(h => getPlainText(h.tokens)); const headers = token.header.map(h => getPlainText(h.tokens));
@@ -318,4 +335,4 @@ export function MarkdownTable({ token, highlight, forceWidth }: Props): React.Re
// Render as a single Ansi block to prevent Ink from wrapping mid-row // Render as a single Ansi block to prevent Ink from wrapping mid-row
return <Ansi>{tableLines.join('\n')}</Ansi>; return <Ansi>{tableLines.join('\n')}</Ansi>;
} });