@@ -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