fix(site): cache shiki highlighters during dev HMR (#1619)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sam Potts
2026-05-29 10:02:13 +10:00
committed by GitHub
co-authored by Cursor
parent b22a9f77b5
commit 2a96b3c285
3 changed files with 38 additions and 7 deletions
@@ -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;
+28 -1
View File
@@ -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<Highlighter>;
client?: Promise<Highlighter>;
}
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<Highlighter>
): Promise<Highlighter> {
const cache = getShikiGlobalCache();
cache[key] ??= factory();
return cache[key];
}
export default function createHighlighter(config: Omit<Parameters<typeof libCreateHighlighter>[0], 'themes'>) {
return libCreateHighlighter({
...config,
@@ -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 `<ServerCode>` 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;