perf(markdown): move code highlighting off the main thread into a Shiki worker
Tokenize closed code blocks in a dedicated Shiki Web Worker instead of calling the shared highlighter synchronously on the UI thread. This removes the one-shot main-thread highlight stall when a code fence closes on a large block. Streaming behavior is unchanged: the open (streaming) fence still renders as plain text and is highlighted once on close. On any worker failure the block keeps its escaped plain code — highlighting never falls back onto the main thread. - Add markdownShikiThemeDefinition (dependency-free CSS-variable theme) so the worker can use the theme without pulling in @pierre/diffs / React. - Add markdown-worker-protocol, markdown-shiki.worker, and the main-thread markdown-worker client. - Route highlightCodeBlocks through the worker; keep the size/VSCode line guard and mermaid skip on the main thread. - Add shiki as a direct dependency (was transitive via @pierre/diffs).
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import MarkdownShikiWorkerUrl from './markdown-shiki.worker.ts?worker&url';
|
||||
import type { MarkdownWorkerRequest, MarkdownWorkerResponse } from './markdown-worker-protocol';
|
||||
|
||||
// Main-thread client for the markdown Shiki worker. Moves syntax tokenization
|
||||
// off the UI thread: a closed code block is shipped to the worker, which returns
|
||||
// ready-to-splice Shiki HTML. On any failure (no worker support, worker crash,
|
||||
// tokenization error) the promise resolves to `null` and the caller keeps the
|
||||
// escaped plain-text code — highlighting never falls back onto the main thread.
|
||||
|
||||
let worker: Worker | undefined;
|
||||
let nextId = 0;
|
||||
const pending = new Map<number, (html: string | null) => void>();
|
||||
|
||||
const failAll = (): void => {
|
||||
pending.forEach((resolve) => resolve(null));
|
||||
pending.clear();
|
||||
worker?.terminate();
|
||||
worker = undefined;
|
||||
};
|
||||
|
||||
const getWorker = (): Worker | undefined => {
|
||||
if (worker) return worker;
|
||||
if (typeof window === 'undefined' || typeof Worker === 'undefined') return undefined;
|
||||
try {
|
||||
worker = new Worker(MarkdownShikiWorkerUrl, { type: 'module' });
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
worker.onmessage = (event: MessageEvent<MarkdownWorkerResponse>) => {
|
||||
const resolve = pending.get(event.data.id);
|
||||
if (!resolve) return;
|
||||
pending.delete(event.data.id);
|
||||
resolve(event.data.type === 'highlight' ? event.data.html : null);
|
||||
};
|
||||
worker.onerror = failAll;
|
||||
worker.onmessageerror = failAll;
|
||||
worker.postMessage({ type: 'init' } satisfies MarkdownWorkerRequest);
|
||||
return worker;
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlight a complete code block in the worker. Resolves to Shiki `<pre>` HTML,
|
||||
* or `null` if highlighting is unavailable or failed (caller keeps plain code).
|
||||
*/
|
||||
export const highlightCodeInWorker = (code: string, lang: string): Promise<string | null> => {
|
||||
const instance = getWorker();
|
||||
if (!instance) return Promise.resolve(null);
|
||||
const id = ++nextId;
|
||||
return new Promise<string | null>((resolve) => {
|
||||
pending.set(id, resolve);
|
||||
instance.postMessage({ type: 'highlight', id, code, lang } satisfies MarkdownWorkerRequest);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user