Skip to content

Commit b79985a

Browse files
committed
perf(mothership): restore static markdown parse for settled chat messages
Settled/reloaded chat messages rendered through Streamdown's streaming parser (remend + incomplete-markdown repair + per-block re-parse, running the rehype raw/sanitize/harden chain once per block). Because rows are virtualized, every up/down scroll remounted the messages crossing the overscan boundary and re-paid that N-block cost — the scroll lag. - Render never-streamed mounts (reloaded history, or an in-session message scrolled out of the virtualized window and back) with mode='static': one whole-document parse instead of streaming's per-block re-parse. In-session streaming keeps the streaming parser for its mount life (no drain flash). - Cache Prism highlight output in a module-level bounded LRU so a code block re-highlights at most once across the unmount/remount virtualization does on scroll (a component useMemo would not survive the unmount).
1 parent 7e975e7 commit b79985a

1 file changed

Lines changed: 43 additions & 15 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import 'prismjs/components/prism-typescript'
1010
import 'prismjs/components/prism-bash'
1111
import 'prismjs/components/prism-css'
1212
import 'prismjs/components/prism-markup'
13+
import type { Grammar } from 'prismjs'
1314
import '@sim/emcn/components/code/code.css'
14-
import { Checkbox, CopyCodeButton, cn, highlight, languages } from '@sim/emcn'
15+
import { Checkbox, CopyCodeButton, cn, languages, highlight as prismHighlight } from '@sim/emcn'
1516
import { decodeVfsSegmentSafe } from '@/lib/copilot/vfs/path-utils'
1617
import { extractTextContent } from '@/lib/core/utils/react-node-text'
1718
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
@@ -161,6 +162,31 @@ function fileIconLabel(ref: string, fallback: string): string {
161162
return fallback
162163
}
163164

165+
/**
166+
* Bounded LRU cache for Prism highlight output. Chat rows are virtualized, so a
167+
* message re-highlights every time it scrolls back into view; a component
168+
* `useMemo` would not survive the unmount, so the cache lives at module scope.
169+
*/
170+
const HIGHLIGHT_CACHE_LIMIT = 512
171+
const highlightCache = new Map<string, string>()
172+
173+
function highlight(code: string, grammar: Grammar | undefined, language: string): string {
174+
const key = `${language}\n${code}`
175+
const cached = highlightCache.get(key)
176+
if (cached !== undefined) {
177+
highlightCache.delete(key)
178+
highlightCache.set(key, cached)
179+
return cached
180+
}
181+
const html = prismHighlight(code, grammar, language)
182+
highlightCache.set(key, html)
183+
if (highlightCache.size > HIGHLIGHT_CACHE_LIMIT) {
184+
const oldest = highlightCache.keys().next().value
185+
if (oldest !== undefined) highlightCache.delete(oldest)
186+
}
187+
return html
188+
}
189+
164190
const MARKDOWN_COMPONENTS = {
165191
table({ children }: { children?: React.ReactNode }) {
166192
return (
@@ -412,9 +438,9 @@ function ChatContentInner({
412438
* position (`E`/`qe` in streamdown 2.5), so a re-parse of unchanged content
413439
* without the animate plugin bails at every unoverridden element (`p`,
414440
* `strong`, `tr`, headings, …) and leaves the stale per-char span DOM in
415-
* place. Every instance renders through the streaming parser (see
416-
* `streamingTree` below) so the remount only sheds the spans, never
417-
* re-interprets the markdown.
441+
* place. The settled instance keeps the streaming parser (`parserTree`
442+
* below) so the remount only sheds the spans, never re-interprets the
443+
* markdown.
418444
*
419445
* The drain is deliberately one-way: a stream that resumes afterwards
420446
* (reconnect/continuation) reveals paced but unfaded, because re-arming
@@ -459,18 +485,19 @@ function ChatContentInner({
459485
}, [isRevealing, animationDrained, streamedThisSession])
460486

461487
/**
462-
* Every mount renders through the streaming parser (remend +
463-
* incomplete-markdown repair + block-split) — `mode='static'` is never used.
464-
* The two pipelines parse edge-case markdown differently (unbalanced fences,
465-
* list continuation across blocks), so a message you watched stream would
466-
* render subtly differently from the same message reloaded from the DB; one
467-
* pipeline makes in-session and refreshed renders byte-identical. The rows
468-
* are virtualized, so only visible messages pay the block-split mount cost.
469-
* `streamingTree` (the remount key and animation props) still drops at
470-
* drain, so a settled instance re-renders through the SAME parser minus the
471-
* per-word animation spans — identical pixels.
488+
* `parserTree` (drives `mode`) stays latched for the mount's life: streaming
489+
* mode is the only one that applies remend/incomplete-markdown repair and
490+
* block-split parsing, so a settled message must KEEP the streaming parser —
491+
* swapping to `mode='static'` at drain re-parses the same source through a
492+
* different pipeline (no remend, whole-doc parse) and visibly flashes on any
493+
* reply with unbalanced markdown. `streamingTree` (drives the remount key
494+
* and animation props) additionally drops at drain, so the settled instance
495+
* re-renders through the SAME parser minus the per-word animation spans —
496+
* byte-identical pixels. Only never-streamed mounts (reloaded history)
497+
* render static.
472498
*/
473-
const streamingTree = (isRevealing || streamedThisSession) && !animationDrained
499+
const parserTree = isRevealing || streamedThisSession
500+
const streamingTree = parserTree && !animationDrained
474501

475502
/**
476503
* One-way fade cutoff (see {@link FADE_MAX_REVEALED_CHARS}). Latched so a
@@ -573,6 +600,7 @@ function ChatContentInner({
573600
>
574601
<Streamdown
575602
key={streamingTree ? 'stream' : 'settled'}
603+
mode={parserTree ? undefined : 'static'}
576604
animated={fadeActive ? STREAM_ANIMATION : false}
577605
isAnimating={streamingTree}
578606
components={MARKDOWN_COMPONENTS}

0 commit comments

Comments
 (0)