mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
perf: 表格渲染效率升级
This commit is contained in:
@@ -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>;
|
||||||
}
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user