#1292·shiki

Docs: minimal Web Worker example for offloading highlighting?

Author: blacklizardCreated Jun 26, 2026Updated Jun 26, 2026

The best-performance guide mentions offloading highlighting to a Web Worker, but the section is still a `` placeholder.

I wired this up recently and it turned out to be pretty small with createHighlighterCore + the JavaScript engine, so I'm sharing a minimal version in case it's a useful starting point (or saves writing one from scratch).

typescript
// shiki-worker.ts
import { createHighlighterCore, type HighlighterCore } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'

let highlighter: Promise<HighlighterCore> | undefined

function getHighlighter() {
  return (highlighter ??= createHighlighterCore({
    themes: [import('@shikijs/themes/github-dark')],
    langs: [import('@shikijs/langs/typescript')],
    engine: createJavaScriptRegexEngine(), // no WASM -> smaller worker, faster start
  }))
}

self.onmessage = async (e: MessageEvent<{ id: number, code: string, lang: string }>) => {
  const { id, code, lang } = e.data
  const hl = await getHighlighter()
  self.postMessage({ id, html: hl.codeToHtml(code, { lang, theme: 'github-dark' }) })
}
typescript
// main.ts — request/response wrapper so concurrent calls don't cross wires
const worker = new Worker(new URL('./shiki-worker.ts', import.meta.url), { type: 'module' })
const pending = new Map<number, (html: string) => void>()
let nextId = 0

worker.onmessage = (e: MessageEvent<{ id: number, html: string }>) =>
  pending.get(e.data.id)?.(e.data.html)

export function highlight(code: string, lang: string): Promise<string> {
  const id = nextId++
  return new Promise((resolve) => {
    pending.set(id, resolve)
    worker.postMessage({ id, code, lang })
  })
}

A few things that a guide would probably want to cover (the parts that weren't obvious):

  • the bundler worker syntax — new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }) (Vite / webpack 5 / modern bundlers)
  • the JS engine vs Oniguruma WASM tradeoff specifically inside a worker
  • matching responses to requests by id, so multiple in-flight highlights don't get swapped
  • tsconfig lib: ["webworker"] for self typing
  • a Node worker_threads variant alongside the browser one

If you'd want a docs page out of this, I'm happy to open a PR — just let me know the shape you'd prefer (which bundlers/frameworks to show, raw postMessage vs Comlink).