diff --git a/site/src/components/Code/clientHighlighter.ts b/site/src/components/Code/clientHighlighter.ts index 6309d584..f1fbe866 100644 --- a/site/src/components/Code/clientHighlighter.ts +++ b/site/src/components/Code/clientHighlighter.ts @@ -3,7 +3,7 @@ import css from 'shiki/langs/css.mjs'; import html from 'shiki/langs/html.mjs'; import javascript from 'shiki/langs/javascript.mjs'; import tsx from 'shiki/langs/tsx.mjs'; -import createHighlighter from './createHighlighter'; +import createHighlighter, { getOrCreateCachedHighlighter } from './createHighlighter'; // Eager module-level Promise — deliberately NOT a top-level `await`. // @@ -15,7 +15,9 @@ import createHighlighter from './createHighlighter'; // avoiding the bug. The Promise starts resolving at import time, giving // it a head start before `client:idle` hydration kicks in. // ClientCode.tsx consumes this via React 19's `use()` hook + Suspense. -const highlighterPromise = createHighlighter({ langs: [bash, html, tsx, css, javascript] }); +const highlighterPromise = getOrCreateCachedHighlighter('client', () => + createHighlighter({ langs: [bash, html, tsx, css, javascript] }) +); export function getClientHighlighter() { return highlighterPromise; diff --git a/site/src/components/Code/createHighlighter.ts b/site/src/components/Code/createHighlighter.ts index 411007d2..b253670d 100644 --- a/site/src/components/Code/createHighlighter.ts +++ b/site/src/components/Code/createHighlighter.ts @@ -1,7 +1,34 @@ -import { createHighlighter as libCreateHighlighter } from 'shiki'; +import { type Highlighter, createHighlighter as libCreateHighlighter } from 'shiki'; import gruvboxDarkHard from 'shiki/themes/gruvbox-dark-hard.mjs'; import gruvboxDarkSoft from 'shiki/themes/gruvbox-dark-soft.mjs'; +const SHIKI_CACHE_SYMBOL = Symbol.for('@videojs/site/shiki'); + +type ShikiCacheKey = 'server' | 'client'; + +interface ShikiGlobalCache { + server?: Promise; + client?: Promise; +} + +function getShikiGlobalCache(): ShikiGlobalCache { + const global = globalThis as typeof globalThis & { + [SHIKI_CACHE_SYMBOL]?: ShikiGlobalCache; + }; + global[SHIKI_CACHE_SYMBOL] ??= {}; + return global[SHIKI_CACHE_SYMBOL]; +} + +/** Reuse one highlighter per role across HMR module re-evaluations in dev. */ +export function getOrCreateCachedHighlighter( + key: ShikiCacheKey, + factory: () => Promise +): Promise { + const cache = getShikiGlobalCache(); + cache[key] ??= factory(); + return cache[key]; +} + export default function createHighlighter(config: Omit[0], 'themes'>) { return libCreateHighlighter({ ...config, diff --git a/site/src/components/Code/serverHighlighter.ts b/site/src/components/Code/serverHighlighter.ts index 5172cb1a..3214cc4d 100644 --- a/site/src/components/Code/serverHighlighter.ts +++ b/site/src/components/Code/serverHighlighter.ts @@ -4,15 +4,17 @@ import html from 'shiki/langs/html.mjs'; import javascript from 'shiki/langs/javascript.mjs'; import tsx from 'shiki/langs/tsx.mjs'; import ts from 'shiki/langs/typescript.mjs'; -import createHighlighter from './createHighlighter'; +import createHighlighter, { getOrCreateCachedHighlighter } from './createHighlighter'; // The highlighter is a long-lived singleton at module scope; it is never // disposed, so every grammar and theme we hand it stays in memory for the // process lifetime. Only pre-load the languages `` actually // renders — adding `bundledLanguages` here would drag ~300 grammars into the // build and dominate first-render cost on code-heavy pages. -const serverHighlighter = await createHighlighter({ - langs: [bash, css, html, javascript, ts, tsx], -}); +const serverHighlighter = await getOrCreateCachedHighlighter('server', () => + createHighlighter({ + langs: [bash, css, html, javascript, ts, tsx], + }) +); export default serverHighlighter;