Skip to content

Commit 3a2b6dd

Browse files
perf: 表格渲染效率升级
1 parent 4ca7a48 commit 3a2b6dd

1 file changed

Lines changed: 52 additions & 35 deletions

File tree

src/components/MarkdownTable.tsx

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,40 @@ function wrapText(text: string, width: number, options?: { hard?: boolean }): st
6565
* 2. Distributing available space proportionally
6666
* 3. Wrapping text within cells (no truncation)
6767
* 4. Properly aligning multi-line rows with borders
68+
*
69+
* Performance: uses per-render caches (formatCache, plainTextCache, wrapCache)
70+
* to avoid redundant formatCell/wrapText calls across the multiple passes
71+
* (width calculation, row line counting, rendering). Wrapped in React.memo
72+
* to skip re-renders when props are unchanged.
6873
*/
69-
export function MarkdownTable({ token, highlight, forceWidth }: Props): React.ReactNode {
74+
export const MarkdownTable = React.memo(function MarkdownTable({
75+
token,
76+
highlight,
77+
forceWidth,
78+
}: Props): React.ReactNode {
7079
const [theme] = useTheme();
7180
const { columns: actualTerminalWidth } = useTerminalSize();
7281
const terminalWidth = forceWidth ?? actualTerminalWidth;
7382

74-
// Format cell content to ANSI string
83+
// Per-render caches — Token[] references are stable within a single token
84+
// prop (from LRU cache in Markdown.tsx), so reference equality is sufficient.
85+
const formatCache = new Map<Token[] | undefined, string>();
86+
const plainTextCache = new Map<Token[] | undefined, string>();
87+
7588
function formatCell(tokens: Token[] | undefined): string {
76-
return tokens?.map(_ => formatToken(_, theme, 0, null, null, highlight)).join('') ?? '';
89+
const cached = formatCache.get(tokens);
90+
if (cached !== undefined) return cached;
91+
const result = tokens?.map(_ => formatToken(_, theme, 0, null, null, highlight)).join('') ?? '';
92+
formatCache.set(tokens, result);
93+
return result;
7794
}
7895

79-
// Get plain text (stripped of ANSI codes)
8096
function getPlainText(tokens: Token[] | undefined): string {
81-
return stripAnsi(formatCell(tokens));
97+
const cached = plainTextCache.get(tokens);
98+
if (cached !== undefined) return cached;
99+
const result = stripAnsi(formatCell(tokens));
100+
plainTextCache.set(tokens, result);
101+
return result;
82102
}
83103

84104
// 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
149169
columnWidths = minWidths.map(w => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH));
150170
}
151171

152-
// Step 4: Calculate max row lines to determine if vertical format is needed
153-
function calculateMaxRowLines(): number {
154-
let maxLines = 1;
155-
// Check header
156-
for (let i = 0; i < token.header.length; i++) {
157-
const content = formatCell(token.header[i]!.tokens);
158-
const wrapped = wrapText(content, columnWidths[i]!, {
159-
hard: needsHardWrap,
160-
});
161-
maxLines = Math.max(maxLines, wrapped.length);
162-
}
163-
// Check rows
164-
for (const row of token.rows) {
165-
for (let i = 0; i < row.length; i++) {
166-
const content = formatCell(row[i]?.tokens);
167-
const wrapped = wrapText(content, columnWidths[i]!, {
168-
hard: needsHardWrap,
169-
});
170-
maxLines = Math.max(maxLines, wrapped.length);
171-
}
172+
// Step 4: Single-pass cell preparation — wraps each cell once, caches results
173+
// for reuse by both row-line counting and rendering.
174+
const wrapCache = new Map<Token[] | undefined, string[]>();
175+
176+
function getWrappedLines(tokens: Token[] | undefined, colIndex: number): string[] {
177+
const cached = wrapCache.get(tokens);
178+
if (cached !== undefined) return cached;
179+
const formatted = formatCell(tokens);
180+
const lines = wrapText(formatted, columnWidths[colIndex]!, {
181+
hard: needsHardWrap,
182+
});
183+
wrapCache.set(tokens, lines);
184+
return lines;
185+
}
186+
187+
// Step 5: Calculate max row lines using cached wrapped results
188+
let maxRowLines = 1;
189+
for (let i = 0; i < token.header.length; i++) {
190+
maxRowLines = Math.max(maxRowLines, getWrappedLines(token.header[i]!.tokens, i).length);
191+
}
192+
for (const row of token.rows) {
193+
for (let i = 0; i < row.length; i++) {
194+
maxRowLines = Math.max(maxRowLines, getWrappedLines(row[i]?.tokens, i).length);
172195
}
173-
return maxLines;
174196
}
175197

176-
// Use vertical format if wrapping would make rows too tall
177-
const maxRowLines = calculateMaxRowLines();
178198
const useVerticalFormat = maxRowLines > MAX_ROW_LINES;
179199

180200
// Render a single row with potential multi-line cells
181201
// Returns an array of strings, one per line of the row
182202
function renderRowLines(cells: Array<{ tokens?: Token[] }>, isHeader: boolean): string[] {
183-
// Get wrapped lines for each cell (preserving ANSI formatting)
184-
const cellLines = cells.map((cell, colIndex) => {
185-
const formattedText = formatCell(cell.tokens);
186-
const width = columnWidths[colIndex]!;
187-
return wrapText(formattedText, width, { hard: needsHardWrap });
188-
});
203+
// Reuse cached wrapped lines — no redundant formatCell/wrapText
204+
const cellLines = cells.map((cell, colIndex) => getWrappedLines(cell.tokens, colIndex));
189205

190206
// Find max number of lines in this row
191207
const maxLines = Math.max(...cellLines.map(lines => lines.length), 1);
@@ -231,6 +247,7 @@ export function MarkdownTable({ token, highlight, forceWidth }: Props): React.Re
231247
}
232248

233249
// Render vertical format (key-value pairs) for extra-narrow terminals
250+
// Uses formatCell cache; wrapping uses terminal-width params (not column widths)
234251
function renderVerticalFormat(): string {
235252
const lines: string[] = [];
236253
const headers = token.header.map(h => getPlainText(h.tokens));
@@ -318,4 +335,4 @@ export function MarkdownTable({ token, highlight, forceWidth }: Props): React.Re
318335

319336
// Render as a single Ansi block to prevent Ink from wrapping mid-row
320337
return <Ansi>{tableLines.join('\n')}</Ansi>;
321-
}
338+
});

0 commit comments

Comments
 (0)