Skip to content

Benchmarks

A CMS or site build can highlight hundreds of blocks with one loaded highlighter. That repeated work is the headline here: on our 14-file corpus, Ferriki was 3.3× faster than Shiki/WASM on Linux x86-64 and 4.1× faster on macOS ARM64. Ferriki and Shiki/WASM produced identical HTML for every repository file on both hosts.

These factors are Shiki's time divided by Ferriki's time. They describe this corpus and these machines; they do not measure total build time or guarantee the same factor for another workload.

Prepared regex

Ferriki can carry regex preparation across process restarts. With Ferroni 2.7, standard grammars arrive with precompiled programs and prepared Eager scanner automata. A fresh highlighter restores that work instead of rebuilding the serialized automata from source. The digest-addressed disk cache lets later builds reuse the download when their cache persists.

An October 24, 2026 local diagnostic compares source Eager with full static preparation on macOS ARM64, using Ferroni 2.7.0 and Rust 1.99.0. Two runs rotate four lanes through 20 fresh-registry samples after one discarded round. The table reports startup medians in milliseconds as run A / run B.

GrammarSource EagerPrepared Eager
C++143.699 / 144.58567.955 / 68.083
Swift33.564 / 34.00415.511 / 15.648
Rust3.629 / 3.6051.680 / 1.677
Python12.986 / 13.0746.559 / 6.613
JSON0.464 / 0.4680.387 / 0.383
CSS36.163 / 36.58712.889 / 12.987
TypeScript72.606 / 72.02423.682 / 23.360

For C++, that is about 53% lower tokenizer startup, from roughly 144 ms to 68 ms. Prepared execution stays comparable to ordinary Eager across these workloads. All lanes must produce identical token arrays; prepared lanes also verify that real groups load without falling back to source scanner assembly.

Startup includes compressed SHA-256 verification, copying, bounded decompression, archive opening, theme and grammar construction, and the first document. Network, file I/O, grammar JSON parsing, HTML rendering, and complete builds are outside the timer. This measures tokenizer restoration versus preparation, not end-to-end CDN startup or deployment speed. Restoration still costs more than source Off. CPU placement and thermals were uncontrolled.

The full diagnostic and both raw captures record execution timings, fixture hashes, package sizes, source provenance, and limits. These local prepared-package results are separate from the older Shiki comparisons below; they are not a new Shiki speedup measurement.

Reused highlighters on two hosts

Reused highlighters: sum of medians for the same 14 files. Milliseconds; lower is faster. Default prefilter enabled.
HostFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engineSpeedup (WASM · JS)
Linux x86-64 · AMD EPYC103.6 ms342.6 msUnsupported3.3× · Unsupported
macOS ARM64 · Apple M4 Pro (Virtual)79.8 ms330.2 msUnsupported4.1× · Unsupported

Linux ran on an AMD EPYC with the blacksmith-4vcpu-ubuntu-2404 profile; macOS ran on an Apple M4 Pro virtual machine with blacksmith-6vcpu-macos-26. These are the same runner profiles used by Ferroni. Both profiles used Ferriki 1.1.1, Shiki 4.5.0, Node 24.21.0, Rust 1.99.1 and Ferroni 2.2.2, with identical source, theme and fixture inputs. The machines are reported separately.

The October 16, 2026 workflow run passed on both hosts. All twelve reports, logs, build receipts and original checksums are preserved in the committed measurement archive. The website reads compact reports generated from that archive; its build checks their provenance and values. The measured revision uses the current Ferriki runtime from main at 2d04039, with the benchmark workflow and comparator updates. Shiki is the current stable npm release, independently of the pinned compatibility suite; a registry check rejects stale comparator pins before a run. This compares current engines; it does not isolate the effect of a Ferroni upgrade.

Process memory

A separate Blacksmith run measures the complete Node process, including Rust and WASM memory. It uses the same two runner profiles as the timing suite, with one dedicated VM per profile. Six fresh processes per engine and workload run serially in rotating order; every process result is retained.

The shared repository cohort contains 12 files and runs 50 passes with one loaded highlighter: 600 rendered documents. YAML is excluded for all engines because Shiki/JS cannot translate the grammar. SQL is also excluded because Shiki/JS differs from native/WASM on that fixture in a fresh isolated process. This memory cohort is separate from the 14-file timing corpus above. The second workload repeats the TypeScript API source with newline separators to at least 256 KiB and renders it ten times. All three engines pass exact HTML SHA-256 and byte-length agreement for both measured workloads before measurement. Each HTML result is consumed and released before the next document.

In this capture, Ferriki's median RSS after the repository workload was 27–31% lower than Shiki/WASM across the two hosts. For the generated large TypeScript case it was 64–68% lower. Against Shiki/JS, the repository values were closer: macOS ranges overlap, while Linux recorded a lower Ferriki range. The tables show all engines and full ranges rather than a universal memory factor. Both hosts used Ferriki 1.1.1, Shiki 4.5.0, Ferroni 2.2.2, Node 24.21.0 and Rust 1.99.1, at measured revision 77bd492.

After initialization

RSS after initialization and explicit V8 GC, before rendering. MiB, median [min–max] over six fresh processes per engine and workload. Absolute whole-process values; lower uses less resident memory. Scroll horizontally on small screens to compare engines.
Host / workloadFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engine
Linux x86-64 · 12 files × 50 passes65.7 [65.6–65.8]92.9 [87.5–104.5]83.4 [82.4–84.9]
Linux x86-64 · large TypeScript × 1062.5 [62.3–62.7]84.8 [80.1–89.3]72.7 [72.5–73.3]
macOS ARM64 · 12 files × 50 passes65.0 [64.9–65.1]87.1 [86.0–90.5]83.6 [82.3–84.5]
macOS ARM64 · large TypeScript × 1061.4 [61.0–61.6]79.3 [79.1–80.0]70.5 [70.1–70.5]

After repeated rendering

RSS after repeated rendering and explicit V8 GC, with the highlighter still loaded. MiB, median [min–max] over six fresh processes per engine and workload. Absolute whole-process values; lower uses less resident memory. Scroll horizontally on small screens to compare engines.
Host / workloadFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engine
Linux x86-64 · 12 files × 50 passes316.8 [316.7–316.9]458.8 [444.6–469.1]361.2 [350.6–382.3]
Linux x86-64 · large TypeScript × 10108.2 [108.0–108.4]334.3 [308.2–335.6]307.6 [306.2–332.2]
macOS ARM64 · 12 files × 50 passes330.5 [330.1–331.5]451.6 [447.4–460.2]332.0 [329.7–339.5]
macOS ARM64 · large TypeScript × 10109.5 [109.2–109.9]306.9 [302.6–323.0]290.2 [285.7–296.1]

Peak process memory

Process-lifetime peak RSS through the final render checkpoint, including startup and setup. MiB, median [min–max] over six fresh processes per engine and workload. Absolute whole-process values; lower uses less resident memory. Scroll horizontally on small screens to compare engines.
Host / workloadFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engine
Linux x86-64 · 12 files × 50 passes318.4 [318.3–318.5]463.1 [448.9–474.4]365.8 [355.2–385.6]
Linux x86-64 · large TypeScript × 10110.6 [110.5–110.8]348.4 [319.3–351.4]318.8 [317.4–340.7]
macOS ARM64 · 12 files × 50 passes332.2 [331.9–333.2]456.4 [452.6–465.0]336.5 [334.0–344.1]
macOS ARM64 · large TypeScript × 10111.2 [110.9–111.6]319.3 [318.5–338.5]299.5 [296.4–301.9]

RSS measures resident memory for the whole process. The peak is the OS high-water mark from startup through the final render checkpoint, including imports, inputs, initialization, rendering and explicit V8 garbage collection. It is not a render-only allocation peak. Initialization and render checkpoints use two explicit V8 collections separated by event-loop turns; no collection is forced between documents. Raw pre-GC values and checkpoints after 1, 10 and 50 repository passes (1, 5 and 10 large-document renders) remain in the archive.

V8 heap measurements alone would miss Rust allocations. The raw reports retain heap, external and ArrayBuffer fields as diagnostics, without adding them to RSS. Native allocators and WASM can retain pages after V8 GC, so retained RSS does not by itself establish a leak. These are bounded sequential workloads; they do not measure allocation counts, concurrent capacity or a production memory limit. Compare engines within each host; the two operating systems account for memory differently.

The memory capture and committed memory archive retain the source revision, engine versions, hardware, build receipts, every worker result and original checksums. The homepage build validates this archive before generating its tables. See the memory method and reproduction steps.

First use and the regex prefilter

The older Blacksmith run below used Ferroni 2.2.2, before prepared packages. It compares locally prepared regex optimizations with the prefilter disabled. Current standard grammars restore prepared optimizations by default; see the prepared-package diagnostic for that startup comparison. regexPrefilter: false disables the prefilter and skips prepared packages. Choose it when creating the highlighter; it is not a per-render option. Rust exposes the same choice through .with_regex_prefilter(false).

Ferriki on the 14-file repository corpus. First use includes imports, setup and rendering; process creation and downloads are excluded. Raw on/off observations, milliseconds.
Host / usePrefilter onPrefilter offOff vs. on
Linux x86-64 · reused103.6 ms136.2 ms31.4%
Linux x86-64 · first use648.1 ms483.9 ms-25.3%
macOS ARM64 · reused79.8 ms118.4 ms48.4%
macOS ARM64 · first use562.8 ms403.5 ms-28.3%

In this run, disabling the prefilter made repeated repository highlighting take 31–48% longer, while full-corpus first use was 25–28% shorter. First use includes package imports, highlighter setup and one render of all 14 files. It excludes process creation and asset downloads, and does not measure the latency of one isolated snippet. The on/off reports ran in separate, fixed-order windows, so their raw differences are observations.

The top-level codeToHtml() shorthand uses a shared singleton. Calling that function once per block does not create a fresh highlighter for every block. See reuse a highlighter and factory options.

JSON and Astro

These curated fixtures measure complete HTML rendering with loaded highlighters. example is one fixture; large repeats it 16 times. The default prefilter is enabled below. Ferriki and both Shiki engines passed exact HTML agreement for every cell.

Warm HTML medians with the default prefilter. Example fixtures and large inputs (16 copies); milliseconds. Ferriki and both Shiki engines produce identical HTML.
Host / documentFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engineWASM / Ferriki
Linux x86-64 · JSON · example0.18 ms0.41 ms0.34 ms2.2×
Linux x86-64 · JSON · large2.91 ms6.74 ms5.75 ms2.3×
Linux x86-64 · Astro · example3.58 ms8.21 ms7.55 ms2.3×
Linux x86-64 · Astro · large19.9 ms69.0 ms63.9 ms3.5×
macOS ARM64 · JSON · example0.16 ms0.36 ms0.29 ms2.2×
macOS ARM64 · JSON · large2.42 ms5.71 ms4.63 ms2.4×
macOS ARM64 · Astro · example1.74 ms5.63 ms5.38 ms3.2×
macOS ARM64 · Astro · large16.2 ms65.8 ms64.7 ms4.1×

What changes when the prefilter is disabled?

On/off measurement windows: percentage change in each engine's warm median. Only Ferriki's configuration changes; Shiki serves as a control. Negative values mean shorter times.
Host / documentFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engineInterpretation
Linux x86-64 · JSON · example2.0%0.3%0.7%raw observation
Linux x86-64 · JSON · large6.2%10.6%7.7%raw observation
Linux x86-64 · Astro · example-16.9%-25.8%-28.1%raw observation
Linux x86-64 · Astro · large38.0%-2.2%-0.5%raw observation
macOS ARM64 · JSON · example-19.1%-15.4%-14.5%raw observation
macOS ARM64 · JSON · large-6.5%-5.8%-5.0%raw observation
macOS ARM64 · Astro · example31.6%-4.5%-3.6%raw observation
macOS ARM64 · Astro · large34.1%-15.7%-10.7%raw observation

On Linux, Ferriki's JSON off timings were 2–6% longer. Astro's example was 17% shorter and its large input 38% longer. The unchanged Shiki controls ranged from 28% faster to 11% slower across these cells. On macOS, Ferriki's JSON timings were 7–19% shorter and Astro's 32–34% longer, while the Shiki controls were 4–16% faster. The reports run in separate fixed-order windows, and the controls also move; these are observations rather than isolated configuration effects. A causal comparison needs repeated, alternating runs on one host.

Method

  1. Same output before timing. Ferriki and both Shiki engines must produce identical serialized HTML for supported cases. Shiki/JS cannot translate the current YAML grammar, so YAML and its full-corpus warm and cold totals are explicitly unsupported. Its other 13 repository files pass exact HTML agreement. Curated and TIOBE reports also check source preservation. Errors, timeouts, missing cases and invalid samples fail the Blacksmith job, with diagnostics retained.
  2. Warm repository timings. One loaded highlighter per engine. Five warmup rounds precede at least 30 samples and a target budget of 1.5 seconds per eligible engine per document. Engine order rotates every round. Totals sum per-document medians; they are not a single timed batch.
  3. Warm curated and TIOBE timings. One fresh worker per language, reused highlighters, five warmup calls, at least 30 rounds, a shared target budget of 300 ms per engine, and a cap of 1,000 rounds. Every sample is retained. Setup diagnostics are excluded and are not fair cold-start comparisons.
  4. Cold repository timings. Ten fresh processes per Node engine. Timing starts before engine-specific imports and includes setup and rendering the corpus once. Eligible engine order rotates each round. Process creation is outside the clock; grammar and theme assets are locally cached.
  5. Independent corpus VMs. Each profile uses one VM per corpus. All engines and both prefilter modes for that corpus run serially on the same VM, with prefilter on then off. Independent corpora run concurrently on separate VMs. A merge validates every report, checksum and build input before joining the evidence. Repository totals and JSON/Astro are separate corpus observations; timings from different VMs are never combined into one comparison. Each VM's compiler, addon hash, source revision and fixture hashes are retained.

The repository corpus contains real files from Ferriki and its unchanged upstream mirrors. The curated corpus covers 20 formats, including embedded languages. The TIOBE selection remains pinned to September 2026; Scratch is explicitly unsupported. Prism uses independent grammars, so its retained timings do not claim TextMate output parity.

The optional Phiki comparison was skipped on both Blacksmith hosts. This run has no PHP performance claim. The repository harness can also measure pinned Phiki 2.2.1 when PHP 8.3+, mbstring/Oniguruma and Composer dependencies are available; it compares visible text and per-character styles separately from the strict Node HTML gate.

Phiki was skipped: Phiki dependencies are not installed; run composer install --no-interaction --working-dir=node/benchmarks/phiki.

Reproduce

Run both paid Blacksmith profiles manually from the repository root:

gh workflow run blacksmith-comparison.yml --ref main -f platform=both

To run the separate memory suite:

gh workflow run blacksmith-comparison.yml --ref main -f platform=both -f suite=memory

Or measure locally, keeping the machine idle:

cd node
pnpm install --frozen-lockfile --ignore-scripts
node scripts/check-benchmark-versions.mjs
node ../scripts/materialize-assets.mjs --sources
pnpm run build:native
node scripts/run-blacksmith-comparison.mjs /tmp/ferriki-comparison
node scripts/run-blacksmith-memory.mjs /tmp/ferriki-memory

To reproduce the recorded inputs, check out the measured revision aea84dc7b911c79a86dd3631c698c991c36184b8 first. A current checkout measures the current sources. Omit the registry freshness check when reproducing a dated archive with its recorded comparator pins. Reports describe workspace builds; they do not certify registry artifacts. The workflow guide covers artifact retention and interpretation.

Linux measurement context

Measured
2026-10-16
Machine
4 × AMD EPYC, 15 GiB, Linux 6.6.141 (linux-x64), Node v24.21.0
Revision
aea84dc7
Versions
Ferriki 1.1.1 (workspace source) · Shiki 4.5.0 · tm-grammars 1.32.3 · tm-themes 1.12.3 · Ferroni 2.2.2 · Phiki skipped
Theme
github-dark
Node HTML agreement
Ferriki (native): 14 of 14 · Shiki + Oniguruma WASM: 14 of 14 · Shiki + JavaScript engine: 13 of 14
Phiki HTML output agreement
Phiki dependencies are not installed; run composer install --no-interaction --working-dir=node/benchmarks/phiki.

Every repository document on Linux

The detailed chart and table below use the Linux report with the default prefilter. The archive includes the same per-file data for macOS.

Linux x86-64 · time to render highlighted HTML14 documents · sum of per-document medians · lower is faster
Ferriki (native)
103.6 ms
Shiki + Oniguruma WASM
342.6 ms
Shiki + JavaScript engine
Not measured

Reused highlighters · github-dark · measured 2026-10-16. Shiki/JS does not support the current YAML grammar; its full-corpus total is unavailable.

Median HTML time per document in milliseconds; lower is faster.
Source fileFerriki (native)Shiki + Oniguruma WASMShiki + JavaScript engineShiki WASM / Ferriki
src/highlighter.rsrust · 1396 lines · Source file8.46 ms51.7 ms30.1 ms6.1×
node/ferriki/src/api.mtstypescript · 321 lines · Source file9.99 ms22.5 ms19.9 ms2.2×
node/ferriki/src/index.mjsjavascript · 1728 lines · Source file37.6 ms203.0 ms137.8 ms5.4×
docs/ferriki-api.mdmarkdown · 501 lines · Source file4.26 ms12.4 ms15.1 ms2.9×
.github/workflows/ci.ymlyaml · 571 lines · Source file35.3 ms28.5 msUnsupported0.8×
node/compat/upstream/shiki/packages/twoslash/style-rich.csscss · 289 lines · Source file2.77 ms12.5 ms3.71 ms4.5×
node/package.jsonjson · 117 lines · Source file0.63 ms1.47 ms1.16 ms2.3×
Cargo.tomltoml · 118 lines · Source file0.51 ms1.17 ms0.85 ms2.3×
node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.pypython · 98 lines · Source file2.21 ms3.70 ms1.73 ms1.7×
node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.htmlhtml · 42 lines · Source file0.94 ms2.20 ms1.63 ms2.3×
node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.gogo · 24 lines · Source file0.16 ms0.55 ms0.31 ms3.5×
node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.phpphp · 46 lines · Source file0.35 ms1.82 ms0.91 ms5.2×
node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.shshellscript · 31 lines · Source file0.40 ms0.98 ms0.67 ms2.4×
node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.sqlsql · 6 lines · Source file0.05 ms0.27 ms0.08 ms6.0×

Rust syntect comparison

This separate Rust experiment compares syntect 5.3.0 with regex-onig and regex-fancy against Ferriki 0.13.0. It measures complete HTML-highlighting calls on the same pinned source documents. Each engine uses its own grammar catalog and dark theme: syntect uses base16-ocean.dark, while Ferriki uses nord. Grammars, scopes, theme colors, and markup differ, so these observations do not establish output parity or a speedup for equivalent work. The Ferriki measurements use the shared 0.13.0 base runtime before the scanner-cache work tracked in issue #230, so they do not include that change.

The pinned set contains 20 formats. Syntect's default syntax set resolved exact grammars for 11; Astro, Svelte, Swift, TSX, Vue, MDX, SCSS, TOML, and TypeScript were excluded without fallback substitutions. The comparison guide covers the catalog and migration boundaries; the capture archive and method contains the resolved names, all 20 fixture hashes, six raw reports, support probes, binary receipts, and checksums.

Fresh-engine setup

Each value is the median of ten engine setups in that capture round. Setup is outside the per-document render timings.

EngineRound ARound B
syntect regex-onig0.593 ms0.607 ms
syntect regex-fancy0.659 ms0.660 ms
Ferriki0.598 ms0.622 ms

First corpus pass

First use is the first corpus pass on a fresh in-process engine instance. Each cell is the median over 20 samples for that document (ten per capture round). Process startup and cold operating-system caches are excluded; engine setup is reported above.

Languagesyntect regex-onigsyntect regex-fancyFerriki
Java2.065 ms15.391 ms12.314 ms
C++7.445 ms71.530 ms365.327 ms
CSS1.393 ms6.011 ms49.253 ms
HTML9.744 ms58.601 ms100.346 ms
JSON0.355 ms1.269 ms2.852 ms
Markdown3.063 ms58.863 ms118.916 ms
Python4.306 ms27.838 ms23.360 ms
Ruby2.628 ms20.382 ms83.645 ms
Rust3.544 ms25.590 ms5.585 ms
Shell (bash)2.276 ms25.439 ms10.790 ms
YAML0.855 ms7.369 ms3.449 ms

Warmed reuse

Each engine uses a separate instance, five untimed corpus warmups, then 30 timed renders per document in each round. Document order rotates; each cell is the median across 60 samples from both rounds.

Languagesyntect regex-onigsyntect regex-fancyFerriki
Java0.678 ms1.289 ms0.449 ms
C++1.123 ms3.016 ms0.890 ms
CSS0.812 ms1.892 ms0.611 ms
HTML0.886 ms2.034 ms0.891 ms
JSON0.302 ms0.273 ms0.296 ms
Markdown0.433 ms3.189 ms0.597 ms
Python1.566 ms3.546 ms0.545 ms
Ruby0.985 ms2.552 ms0.379 ms
Rust0.682 ms1.796 ms0.368 ms
Shell (bash)0.706 ms2.144 ms0.517 ms
YAML0.534 ms1.875 ms0.293 ms

Both rounds ran serially on an Apple M1 Ultra with Rust 1.99.0; the engine order was reversed across rounds. Every report verifies its binary hash did not change during capture, checks source preservation and highlighted spans, and records an unchanged manifest for all 20 fixture files. HTML output and colors were not compared. These per-language values describe this pinned corpus and this host, not the performance of another application.