Files
openchamber/packages/ui/src/components/chat/markdown/markdown-shiki.worker.ts
T
Bohdan Triapitsyn 464c4ac0ca 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).
2026-06-16 01:07:43 +03:00

56 lines
2.0 KiB
TypeScript

/// <reference lib="webworker" />
import { bundledLanguages, createHighlighter, type BundledLanguage } from 'shiki';
import { MARKDOWN_SHIKI_THEME, MARKDOWN_SHIKI_THEME_DEFINITION } from './markdownShikiThemeDefinition';
import type { MarkdownWorkerRequest, MarkdownWorkerResponse } from './markdown-worker-protocol';
// Single shared highlighter for the worker. Languages load lazily on demand.
let highlighter: ReturnType<typeof createHighlighter> | undefined;
// Serialize work so language loading / tokenization never overlaps.
let queue = Promise.resolve();
const ensureHighlighter = (): ReturnType<typeof createHighlighter> => {
highlighter ??= createHighlighter({
// Cast: the theme is a CSS-variable TextMate theme; Shiki accepts the shape.
themes: [MARKDOWN_SHIKI_THEME_DEFINITION as unknown as Parameters<typeof createHighlighter>[0]['themes'][number]],
langs: [],
});
return highlighter;
};
self.onmessage = (event: MessageEvent<MarkdownWorkerRequest>) => {
const request = event.data;
if (request.type === 'init') {
ensureHighlighter();
return;
}
queue = queue.then(() => highlight(request)).catch(() => {});
};
async function highlight(request: Extract<MarkdownWorkerRequest, { type: 'highlight' }>): Promise<void> {
try {
const instance = await ensureHighlighter();
let lang = request.lang in bundledLanguages ? request.lang : 'text';
if (lang !== 'text' && !instance.getLoadedLanguages().includes(lang)) {
try {
await instance.loadLanguage(bundledLanguages[lang as BundledLanguage]);
} catch {
lang = 'text';
}
}
const html = instance.codeToHtml(request.code, {
lang,
theme: MARKDOWN_SHIKI_THEME,
tabindex: false,
});
post({ type: 'highlight', id: request.id, html });
} catch (error) {
post({ type: 'error', id: request.id, message: error instanceof Error ? error.message : String(error) });
}
}
function post(response: MarkdownWorkerResponse): void {
self.postMessage(response);
}