syntax-highlighter-bench

This is a benchmark of JS syntax highlighters: fuz_code, Twinkleplop, Prism, and Shiki with each of its two engines, run on the same inputs through one adapter each. It times throughput and cold start on one named machine, and measures bundle size, install footprint, retained memory, token counts, and output size, which come out the same on any machine with the same Node version.

The maintainer of this benchmark also maintains fuz_code, so that library is marked wherever it appears, every input is labeled by where it came from, and the method is published for anyone to reproduce.

The libraries have different features. Shiki highlights with the TextMate grammars and themes VS Code uses; Twinkleplop aims for a similar feature set, with its own themes, annotations, and Twoslash support; and both do more than fuz_code, which has the fewest features of the four: a single-pass lexer per language, with CSS classes for its theme. Their fidelity differs too: how finely each splits the same source, and how accurately it classifies each part. These numbers compare the work all of them share, not what one offers that another doesn't, and they time each library's output without scoring its quality.

Every result, with filters · how it is measured · the inputs · the adapters, and how to add a library

AI disclosure: this is an LLM-generated repo guided by a person.

Headline figures

Each bar's ratio is to the best library in the chart, where 1.00× is the best and a ratio marked ≈ is inside the row's noise floor: the machine's for throughput, and the heap check's tolerance for retained memory. Hover a bar to compare the others against it instead.

How many times the baseline a tint means:
  • under 0.9×, better than the reference
  • 0.9–1.1×, level
  • 1.1–2×
  • 2–5×
  • 5–10×
  • 10–100×
  • 100× or more
Tokenizing
fuz_codemaintainer's 234 MB/s 1.00×
Twinkleplop 64.3 MB/s 3.65×
Prism 14.8 MB/s 15.8×
Shiki (JS engine) 1.24 MB/s 189×
Shiki (wasm) 1.27 MB/s 185×

higher is better — MB/s over the neutral inputs, geometric mean over 27 of 27 rows

Highlighting to HTML
fuz_codemaintainer's 69.0 MB/s 1.00×
Twinkleplop 34.8 MB/s 1.98×
Prism 7.91 MB/s 8.73×
Shiki (JS engine) 0.86 MB/s 80.5×
Shiki (wasm) 0.94 MB/s 73.6×

higher is better — MB/s over the neutral inputs, geometric mean over 27 of 27 rows

Cold start to a first highlight
fuz_codemaintainer's 2.72 ms 1.00×
Twinkleplop 14.4 ms 5.29×
Prism 7.72 ms 2.84×
Shiki (JS engine) 139 ms 51.2×
Shiki (wasm) 128 ms 47.1×

lower is better — ms a bundled library adds to a fresh Node process, geometric mean over the languages over 7 of 9 rows: the others lack an exact number for a library and are left out

Retained memory
fuz_codemaintainer's 158 kB 1.00×
Twinkleplop 1.25 MB 7.93×
Prism 292 kB 1.84×
Shiki (JS engine) 4.05 MB 25.6×
Shiki (wasm) 26.6 MB 168×

lower is better — what a library holds after loading one language and highlighting a small snippet once: V8 heap, machine code, and external memory, geometric mean over the languages over 9 of 9 rows

Everything a page loads
fuz_codemaintainer's 14.0 kB 1.15× JS 13.4 kB · no wasm binary · theme stylesheet 643 B · theme: light + dark
Twinkleplop 51.2 kB 4.21× JS 49.1 kB · no wasm binary · theme stylesheet 2.13 kB · theme: light + dark
Prism 12.2 kB 1.00× JS 11.5 kB · no wasm binary · theme stylesheet 657 B · theme: light
Shiki (JS engine) 122 kB 10.0× JS 122 kB · no wasm binary · no stylesheet: the theme is in the JS · theme: light
Shiki (wasm) 263 kB 21.6× JS 104 kB · wasm binary 159 kB · no stylesheet: the theme is in the JS · theme: light

lower is better — the JS, wasm binary, and theme stylesheet of the docs set (js, ts, css, html, md, bash, json), each compressed on its own with gzip

The JS alone
fuz_codemaintainer's 13.4 kB 1.16×
Twinkleplop 49.1 kB 4.26×
Prism 11.5 kB 1.00×
Shiki (JS engine) 122 kB 10.6×
Shiki (wasm) 104 kB 9.06×

lower is better — the docs set (js, ts, css, html, md, bash, json), minified and compressed with gzip

Timed on laptop1 (AMD Ryzen 5 PRO 7530U with Radeon Graphics, Node v24.14.1) on 2026-10-11 at commit bd12c165af. The machine's noise floor is 2.3% for one cell and 0.3% for a geometric mean: a smaller difference is not a result. See the method for what each number means.

The machine and the run

laptop1: AMD Ryzen 5 PRO 7530U with Radeon Graphics, 12 threads, performance governor, amd-pstate-epp driver, energy preference performance, boost on, 14.4 GB, kernel 6.12.90+deb13.1-amd64. Corpus e649d70d43. The anchor, a fixed workload read before and after measuring, drifted 2.2%: stable. Two processes measuring the same library on the same cell in this run were 4.0% apart at the 95th percentile and 0.9% at the median, against 4.1% at the 95th percentile in the calibration.

These are summaries. The results page has every cell behind them, on inputs from other sources too. The caveats say what the numbers don't: the libraries don't do identical work, and Shiki's HTML carries inline styles where the others emit classes.

What the numbers mean in practice

Whether a ratio matters depends on where the highlighting runs and how often. Which figure to read first:

  • At build time, as a static site or documentation build highlights its pages once: throughput sets the build's time and nothing else. Readers get plain HTML, so the JS and the cold start never reach them, though a theme stylesheet still does.
  • In the browser: everything a page loads and the cold start to a first highlight come first, then throughput on long pages.
  • In an editor that highlights as you type: throughput at the size being edited, against the time of one frame.
  • On a server that renders each request: throughput and retained memory, paid on every request and held by every process.
  • In serverless and edge functions: the cold start, paid by every fresh instance, and the bundle where the platform limits its size.

The HTML throughput worked out for sizes a reader knows. Each figure applies the geometric-mean rate over the neutral inputs to that size, so it is an estimate; a library's rate differs from input to input, as the results page shows.

worked out forfuz_codemaintainer's Twinkleplop Prism Shiki (JS engine) Shiki (wasm)
Highlighting to HTML, at the neutral geometric mean over 27 of 27 rows
one 10 kB code block 0.14 ms 1.00× 0.29 ms 1.98× 1.26 ms 8.73× 11.7 ms 80.5× 10.7 ms 73.6×
10 kB blocks in one 16 ms frame whole blocks 110 1.00× 55 2.00× 12 9.17× 1 110× 1 110×
1 MB of source as a site build might highlight 14.5 ms 1.00× 28.7 ms 1.98× 126 ms 8.73× 1,167 ms 80.5× 1,067 ms 73.6×

What each language costs

What each language's grammar costs, in two ways. The first language is the JS a page loads for that language alone, minified and compressed with gzip, less the library with no language: it includes any runtime the language needs that the floor leaves out, which a library pays once, and any language it loads with it, as when a library's TypeScript loads its JS. One more language is what the language adds to a bundle that already holds every other timed language, in minified bytes: the price of adding it to a site that has the rest. A language the others already hold, like JS inside TypeScript, adds little or nothing there. The bars of each group are on one scale, and beside a number is its ratio to the smallest in the row; a row where a library adds nothing has no ratios.

languagefuz_codemaintainer's Twinkleplop Prism Shiki (JS engine) Shiki (wasm)
The floor the library with no language
core 1.31 kB 1.00× 16.4 kB 12.5× 3.56 kB 2.71× 56.4 kB 42.9× 38.8 kB 29.6×
The first language the language alone, less the floor: with any runtime the floor leaves out
TypeScript 4.80 kB 2.26× 27.7 kB 13.0× 2.13 kB 1.00× 15.6 kB 7.35× 15.7 kB 7.36×
JS 4.80 kB 2.68× 24.9 kB 13.9× 1.79 kB 1.00× 16.1 kB 8.98× 16.1 kB 9.00×
CSS 1.19 kB 2.15× 11.0 kB 19.9× 552 B 1.00× 11.8 kB 21.3× 11.8 kB 21.4×
HTML 7.76 kB 2.56× 24.9 kB 8.20× 3.04 kB 1.00× 38.7 kB 12.8× 39.0 kB 12.8×
XML not timed 2.34 kB 2.47× — unsupported: no xml949 B 1.00× 4.94 kB 5.20× 4.97 kB 5.24×
JSON 695 B 3.26× 606 B 2.85× 213 B 1.00× 610 B 2.86× 615 B 2.89×
Svelte 8.44 kB 2.48× 25.9 kB 7.63× 3.40 kB 1.00× 46.6 kB 13.7× 46.5 kB 13.7×
Markdown 3.68 kB 1.59× 2.31 kB 1.00× 2.65 kB 1.15× 5.55 kB 2.40× 5.57 kB 2.41×
Bash 3.36 kB 1.11× 12.8 kB 4.20× 3.04 kB 1.00× 5.90 kB 1.94× 5.93 kB 1.95×
Rust 2.58 kB 2.32× 13.7 kB 12.3× 1.11 kB 1.00× 2.55 kB 2.29× 2.56 kB 2.30×
One more language every timed language, less all of them but this one, in minified bytes: near zero when the others already bring it in, as HTML and Svelte bring their script and style languages
TypeScript 0 B 11.1 kB 1.29 kB 3 B 3 B
JS 0 B 109 B 0 B 3 B 3 B
CSS 0 B 95 B 0 B 3 B 3 B
HTML 88 B 97 B 0 B 57.6 kB 57.6 kB
JSON 1.39 kB 3.09× 1.03 kB 2.29× 449 B 1.00× 2.80 kB 6.23× 2.80 kB 6.23×
Svelte 2.39 kB 1.00× 4.81 kB 2.02× 2.55 kB 1.07× 24.5 kB 10.2× 24.5 kB 10.2×
Markdown 4.38 kB 1.00× 6.73 kB 1.54× 5.24 kB 1.19× 59.3 kB 13.5× 59.3 kB 13.5×
Bash 8.09 kB 1.32× 7.67 kB 1.25× 6.14 kB 1.00× 41.5 kB 6.75× 41.5 kB 6.75×
Rust 6.29 kB 2.53× 10.6 kB 4.28× 2.49 kB 1.00× 15.0 kB 6.05× 15.0 kB 6.05×

Coverage

Which languages each library is measured on. They are fuz_code's built-in set, so the list favors it: a language fuz_code doesn't have is never measured. Prism and Shiki each support hundreds of languages, and none of that shows here.
languagefuz_codemaintainer'sTwinkleplopPrismShiki (JS engine)Shiki (wasm)
TypeScript ts yesyesyesyesyes
JS js yes1yesyesyesyes
CSS css yesyesyesyesyes
HTML html yesyesyesyesyes
XML xml not timedyes— unsupportedyes2yes3yes3
JSON json yesyesyesyesyes
Svelte svelte yesyesyes4yesyes
Markdown md yesyesyesyesyes
Bash bash yesyesyesyesyes
Rust rust yesyesyesyesyes
  1. the TypeScript lexer, which fuz_code registers for JS
  2. the markup grammar, which Prism also registers as xml
  3. the XML grammar embeds the Java grammar, which loads with it
  4. the third-party prism-svelte grammar

Unsupported means the library's adapter doesn't claim the language, and is never shown as a zero or counted in a mean. Languages and sets has the details.

Libraries

The measured libraries, at the versions these results came from.
libraryversionHTML outputenginedefault theme's color schemeswhat is measured
fuz_code fuz_code
maintainer's this benchmark's maintainer also maintains it
@fuzdev/fuz_code 0.52.1 classessingle-pass lexerslight + dark

Single-pass lexers. tokenize is the lexer alone and html adds the renderer.

Twinkleplop twinkleplop @twinkleplop/core 0.2.2
with
  • @twinkleplop/bash 0.1.5
  • @twinkleplop/css 0.1.5
  • @twinkleplop/html 0.1.5
  • @twinkleplop/javascript 0.1.5
  • @twinkleplop/json 0.1.5
  • @twinkleplop/markdown 0.1.5
  • @twinkleplop/rust 0.1.5
  • @twinkleplop/svelte 0.1.5
  • @twinkleplop/typescript 0.1.5
  • @twinkleplop/theme-github 0.2.2
classesgrammar VMlight + dark

The full pipeline: the grammar plus the language's reclassifier stack, as shipped.

Prism prism prismjs 1.30.0
with
  • prism-svelte 0.5.0
classesregex grammarslight

The core plus one component file per grammar, without the autoloader or any plugin.

Shiki (JS engine) shiki_js shiki 4.4.3 inline stylesTextMate grammarslight

The JS RegExp engine. The HTML carries inline styles rather than classes.

Shiki (wasm) shiki_wasm shiki 4.4.3 inline stylesTextMate grammarslight

The Oniguruma wasm engine, Shiki's default. The HTML carries inline styles rather than classes.

Each library is driven through one adapter with notes on the entry points it uses, so its maintainers can review or replace it. See the adapters.

every result, with filters
how it is measured
the inputs
the adapters, and how to add a library