perf(desktop): load shiki and @streamdown/code on first use, not at boot

The chunk split only helps if nothing on the entry graph statically
imports the heavy libs. Three seams did:

- react-shiki: now behind one lazy() boundary (shiki-block.tsx is its only
  static importer; LazyShiki wraps it with a PlainCode fallback).
- diff-lines.tsx: useShikiHighlighter hook extracted to a lazily-loaded
  syntax-diff.tsx (plain DiffBody fallback — same output Shiki's own
  pre-resolve state showed); codeToTokens becomes a dynamic import that
  runs only once a highlightable diff is on screen.
- @streamdown/code: statically ran createJavaScriptRegexEngine() and built
  full language registries at module scope on every launch. It now loads
  via useCodePlugin() on first markdown mount and swaps into the plugin
  table when it lands; fenced code renders plain until then, identical to
  the delay fallback users already see during streaming.

Interleaved A/B vs main (prod build, warm V8 cache, median of 4, two
alternating rounds): FCP 1100->940ms, DOMContentLoaded 618->542ms,
nav-to-interactive 1312->1123ms on the quiet round; same direction with
larger gaps under load. ~160-190ms off every renderer boot metric.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 20:17:38 -05:00
parent 6fb5d2d89c
commit 1baea21cfb
6 changed files with 120 additions and 24 deletions

View file

@ -7,7 +7,6 @@ import type {
ReactNode
} from 'react'
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ShikiHighlighter from 'react-shiki'
import { Streamdown } from 'streamdown'
import { requestComposerFocus, requestComposerInsertRefs } from '@/app/chat/composer/focus'
@ -18,6 +17,7 @@ import { RichCodeBlock } from '@/components/assistant-ui/embeds'
import { CodeEditor } from '@/components/chat/code-editor'
import { FileDiffPanel } from '@/components/chat/diff-lines'
import { chunkTextLines, useFixedRowWindow } from '@/components/chat/fixed-row-window'
import { LazyShiki as ShikiHighlighter } from '@/components/chat/shiki-highlighter'
import { PageLoader } from '@/components/page-loader'
import { Tip } from '@/components/ui/tooltip'
import { translateNow, useI18n } from '@/i18n'

View file

@ -7,7 +7,7 @@ import {
type SyntaxHighlighterProps,
tailBoundedRemend
} from '@assistant-ui/react-streamdown'
import { code } from '@streamdown/code'
import type { code as streamdownCode } from '@streamdown/code'
import { type ComponentProps, memo, useEffect, useMemo, useState } from 'react'
import { ExpandableBlock } from '@/components/chat/expandable-block'
@ -52,6 +52,41 @@ import { detectEmbed, extractAlert, MarkdownAlert, RichCodeBlock, UrlEmbed } fro
// LLM convention). The default false-setting only accepts `$$...$$`.
const mathPlugin = createMemoizedMathPlugin({ singleDollarTextMath: true })
// `@streamdown/code` statically imports ALL of shiki (every grammar + theme —
// the single largest chunk in the renderer), so it must never sit on the
// entry graph. Load it on first markdown mount and swap it into the plugin
// table when it lands; until then fenced code renders through the
// `SyntaxHighlighter` override's plain path (same output Shiki's own
// `delay` fallback shows), so nothing flashes or reflows unexpectedly.
type CodePlugin = typeof streamdownCode
let codePluginCache: CodePlugin | null = null
function useCodePlugin(): CodePlugin | null {
const [plugin, setPlugin] = useState(codePluginCache)
useEffect(() => {
if (plugin) {
return
}
let cancelled = false
void import('@streamdown/code').then(({ code }) => {
codePluginCache = code
if (!cancelled) {
setPlugin(code)
}
})
return () => {
cancelled = true
}
}, [plugin])
return plugin
}
// Replaces Streamdown's `parseIncompleteMarkdown` (full-text remend per
// flush) with a tail-bounded repair. Must stay module-scope so the prop
// identity is stable across renders.
@ -419,8 +454,10 @@ function MarkdownTextSurface({
// Keep code parsing enabled while streaming so incomplete fenced blocks still
// render as code cards. The expensive Shiki pass is deferred by
// `SyntaxHighlighter` below when `isStreaming` is true.
const plugins = useMemo(() => ({ math: mathPlugin, code }), [])
// `SyntaxHighlighter` below when `isStreaming` is true, and the code plugin
// itself arrives async (useCodePlugin) so shiki never blocks cold start.
const code = useCodePlugin()
const plugins = useMemo(() => (code ? { math: mathPlugin, code } : { math: mathPlugin }), [code])
const components = useMemo(
() =>

View file

@ -1,9 +1,7 @@
'use client'
import type { ReactNode } from 'react'
import * as React from 'react'
import { useShikiHighlighter } from 'react-shiki'
import { type BundledLanguage, codeToTokens, type ShikiTransformer, type ThemedToken } from 'shiki'
import type { BundledLanguage, ShikiTransformer, ThemedToken } from 'shiki'
import { chunkLines, type LineChunk, useFixedRowWindow } from '@/components/chat/fixed-row-window'
import { exceedsHighlightBudget, SHIKI_THEME } from '@/components/chat/shiki-highlighter'
@ -276,7 +274,8 @@ function parseFullFileDiff(diff: string, fullText: string): DiffLine[] {
return out
}
function DiffBody({ lines, syntax }: { lines: DiffLine[]; syntax?: boolean }) {
/** Exported for the lazily-loaded SyntaxDiff (syntax-diff.tsx). */
export function DiffBody({ lines, syntax }: { lines: DiffLine[]; syntax?: boolean }) {
return (
<>
{lines.map((line, index) => (
@ -383,7 +382,10 @@ function TokenizedDiffBody({
let cancelled = false
setTokens(null)
void codeToTokens(code, { lang: language as BundledLanguage, theme })
// Dynamic import so the multi-MB shiki chunk stays off the cold-start
// path — this effect only runs once a highlightable diff is on screen.
void import('shiki')
.then(({ codeToTokens }) => codeToTokens(code, { lang: language as BundledLanguage, theme }))
.then(result => {
if (!cancelled) {
setTokens(result.tokens)
@ -446,7 +448,8 @@ function TokenizedDiffBody({
// Shiki transformer: tag each `.line` with the diff tint for its kind, so the
// syntax-highlighted output keeps add/remove backgrounds + the gutter accent.
function diffLineTransformer(kinds: DiffKind[]): ShikiTransformer {
// Exported for the lazily-loaded SyntaxDiff (syntax-diff.tsx).
export function diffLineTransformer(kinds: DiffKind[]): ShikiTransformer {
return {
line(node, line) {
const kind = kinds[line - 1] ?? 'context'
@ -463,18 +466,18 @@ function diffLineTransformer(kinds: DiffKind[]): ShikiTransformer {
}
function SyntaxDiff({ language, lines }: { language: string; lines: DiffLine[] }) {
const code = React.useMemo(() => lines.map(line => line.text).join('\n'), [lines])
const transformers = React.useMemo(() => [diffLineTransformer(lines.map(line => line.kind))], [lines])
const highlighted = useShikiHighlighter(code, language, SHIKI_THEME, {
defaultColor: 'light-dark()',
transformers
})
// Until Shiki resolves, show the plain colored diff so there's no flash.
return (highlighted as ReactNode) ?? <DiffBody lines={lines} />
// The Shiki hook lives in a lazily-loaded module (syntax-diff.tsx) so the
// multi-MB shiki chunk stays off the cold-start path. Until it (and the
// highlight itself) resolves, show the plain colored diff — no flash.
return (
<React.Suspense fallback={<DiffBody lines={lines} />}>
<LazySyntaxDiff language={language} lines={lines} />
</React.Suspense>
)
}
const LazySyntaxDiff = React.lazy(() => import('./syntax-diff'))
interface DiffLinesProps extends Omit<React.ComponentProps<'pre'>, 'children'> {
text: string
}

View file

@ -0,0 +1,15 @@
'use client'
/**
* The ONLY static importer of `react-shiki` (and through it the multi-MB
* shiki language/theme bundle). Every consumer reaches this module through
* `React.lazy(() => import('./shiki-block'))` — see `LazyShiki` in
* shiki-highlighter.tsx — so the shiki chunk stays entirely off the
* cold-start path and loads on the first highlighted code block instead.
*
* Do NOT import this module statically from anything the entry graph
* reaches, or the chunk moves back into boot.
*/
import ShikiHighlighter from 'react-shiki'
export default ShikiHighlighter

View file

@ -1,8 +1,8 @@
'use client'
import type { SyntaxHighlighterProps } from '@assistant-ui/react-streamdown'
import { type FC, useMemo } from 'react'
import ShikiHighlighter from 'react-shiki'
import { type ComponentProps, type FC, lazy, Suspense, useMemo } from 'react'
import type ShikiHighlighter from 'react-shiki'
import {
CodeCard,
@ -52,6 +52,20 @@ const MAX_HIGHLIGHT_LINES = 3_000
const CHUNK_LINES = 200
const EST_LINE_PX = 16
// react-shiki (and through it the multi-MB shiki grammar/theme bundle) is the
// heaviest dependency in the renderer. `shiki-block.tsx` is its only static
// importer, so this lazy() is the single seam that keeps shiki out of the
// entry chunk — it loads on the first highlighted code block, not at boot.
const ShikiBlock = lazy(() => import('./shiki-block'))
/** Drop-in ShikiHighlighter that suspends on first use and renders the code
* as plain preformatted text until the shiki chunk arrives. */
export const LazyShiki: FC<ComponentProps<typeof ShikiHighlighter>> = props => (
<Suspense fallback={<PlainCode code={String(props.children ?? '')} />}>
<ShikiBlock {...props} />
</Suspense>
)
export function exceedsHighlightBudget(code: string): boolean {
if (code.length > MAX_HIGHLIGHT_CHARS) {
return true
@ -161,7 +175,7 @@ export const SyntaxHighlighter: FC<HermesSyntaxHighlighterProps> = ({
{plain ? (
<PlainCode code={trimmed} />
) : (
<ShikiHighlighter
<LazyShiki
addDefaultStyles={false}
as="div"
colorReplacements={SHIKI_COLOR_REPLACEMENTS}
@ -172,7 +186,7 @@ export const SyntaxHighlighter: FC<HermesSyntaxHighlighterProps> = ({
theme={SHIKI_THEME}
>
{trimmed}
</ShikiHighlighter>
</LazyShiki>
)}
</Pre>
</ExpandableBlock>

View file

@ -0,0 +1,27 @@
'use client'
/**
* The Shiki-highlighted compact diff body, split out of diff-lines.tsx so the
* `react-shiki` static import (and the multi-MB shiki chunk behind it) loads
* lazily on first use instead of on the cold-start path. diff-lines.tsx
* reaches this through `React.lazy` with a plain `DiffBody` fallback.
*/
import type { ReactNode } from 'react'
import { useMemo } from 'react'
import { useShikiHighlighter } from 'react-shiki'
import { DiffBody, type DiffLine, diffLineTransformer } from '@/components/chat/diff-lines'
import { SHIKI_THEME } from '@/components/chat/shiki-highlighter'
export default function SyntaxDiff({ language, lines }: { language: string; lines: DiffLine[] }) {
const code = useMemo(() => lines.map(line => line.text).join('\n'), [lines])
const transformers = useMemo(() => [diffLineTransformer(lines.map(line => line.kind))], [lines])
const highlighted = useShikiHighlighter(code, language, SHIKI_THEME, {
defaultColor: 'light-dark()',
transformers
})
// Until Shiki resolves, show the plain colored diff so there's no flash.
return (highlighted as ReactNode) ?? <DiffBody lines={lines} />
}