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.
| Grammar | Source Eager | Prepared Eager |
|---|---|---|
| C++ | 143.699 / 144.585 | 67.955 / 68.083 |
| Swift | 33.564 / 34.004 | 15.511 / 15.648 |
| Rust | 3.629 / 3.605 | 1.680 / 1.677 |
| Python | 12.986 / 13.074 | 6.559 / 6.613 |
| JSON | 0.464 / 0.468 | 0.387 / 0.383 |
| CSS | 36.163 / 36.587 | 12.889 / 12.987 |
| TypeScript | 72.606 / 72.024 | 23.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
| Host | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine | Speedup (WASM · JS) |
|---|---|---|---|---|
| Linux x86-64 · AMD EPYC | 103.6 ms | 342.6 ms | Unsupported | 3.3× · Unsupported |
| macOS ARM64 · Apple M4 Pro (Virtual) | 79.8 ms | 330.2 ms | Unsupported | 4.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
| Host / workload | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine |
|---|---|---|---|
| Linux x86-64 · 12 files × 50 passes | 65.7 [65.6–65.8] | 92.9 [87.5–104.5] | 83.4 [82.4–84.9] |
| Linux x86-64 · large TypeScript × 10 | 62.5 [62.3–62.7] | 84.8 [80.1–89.3] | 72.7 [72.5–73.3] |
| macOS ARM64 · 12 files × 50 passes | 65.0 [64.9–65.1] | 87.1 [86.0–90.5] | 83.6 [82.3–84.5] |
| macOS ARM64 · large TypeScript × 10 | 61.4 [61.0–61.6] | 79.3 [79.1–80.0] | 70.5 [70.1–70.5] |
After repeated rendering
| Host / workload | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine |
|---|---|---|---|
| Linux x86-64 · 12 files × 50 passes | 316.8 [316.7–316.9] | 458.8 [444.6–469.1] | 361.2 [350.6–382.3] |
| Linux x86-64 · large TypeScript × 10 | 108.2 [108.0–108.4] | 334.3 [308.2–335.6] | 307.6 [306.2–332.2] |
| macOS ARM64 · 12 files × 50 passes | 330.5 [330.1–331.5] | 451.6 [447.4–460.2] | 332.0 [329.7–339.5] |
| macOS ARM64 · large TypeScript × 10 | 109.5 [109.2–109.9] | 306.9 [302.6–323.0] | 290.2 [285.7–296.1] |
Peak process memory
| Host / workload | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine |
|---|---|---|---|
| Linux x86-64 · 12 files × 50 passes | 318.4 [318.3–318.5] | 463.1 [448.9–474.4] | 365.8 [355.2–385.6] |
| Linux x86-64 · large TypeScript × 10 | 110.6 [110.5–110.8] | 348.4 [319.3–351.4] | 318.8 [317.4–340.7] |
| macOS ARM64 · 12 files × 50 passes | 332.2 [331.9–333.2] | 456.4 [452.6–465.0] | 336.5 [334.0–344.1] |
| macOS ARM64 · large TypeScript × 10 | 111.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).
| Host / use | Prefilter on | Prefilter off | Off vs. on |
|---|---|---|---|
| Linux x86-64 · reused | 103.6 ms | 136.2 ms | 31.4% |
| Linux x86-64 · first use | 648.1 ms | 483.9 ms | -25.3% |
| macOS ARM64 · reused | 79.8 ms | 118.4 ms | 48.4% |
| macOS ARM64 · first use | 562.8 ms | 403.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.
| Host / document | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine | WASM / Ferriki |
|---|---|---|---|---|
| Linux x86-64 · JSON · example | 0.18 ms | 0.41 ms | 0.34 ms | 2.2× |
| Linux x86-64 · JSON · large | 2.91 ms | 6.74 ms | 5.75 ms | 2.3× |
| Linux x86-64 · Astro · example | 3.58 ms | 8.21 ms | 7.55 ms | 2.3× |
| Linux x86-64 · Astro · large | 19.9 ms | 69.0 ms | 63.9 ms | 3.5× |
| macOS ARM64 · JSON · example | 0.16 ms | 0.36 ms | 0.29 ms | 2.2× |
| macOS ARM64 · JSON · large | 2.42 ms | 5.71 ms | 4.63 ms | 2.4× |
| macOS ARM64 · Astro · example | 1.74 ms | 5.63 ms | 5.38 ms | 3.2× |
| macOS ARM64 · Astro · large | 16.2 ms | 65.8 ms | 64.7 ms | 4.1× |
What changes when the prefilter is disabled?
| Host / document | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine | Interpretation |
|---|---|---|---|---|
| Linux x86-64 · JSON · example | 2.0% | 0.3% | 0.7% | raw observation |
| Linux x86-64 · JSON · large | 6.2% | 10.6% | 7.7% | raw observation |
| Linux x86-64 · Astro · example | -16.9% | -25.8% | -28.1% | raw observation |
| Linux x86-64 · Astro · large | 38.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 · example | 31.6% | -4.5% | -3.6% | raw observation |
| macOS ARM64 · Astro · large | 34.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
- 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.
- 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.
- 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.
- 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.
- 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.
- Reproduce
- Method and reproduction command
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.
- 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.
| Source file | Ferriki (native) | Shiki + Oniguruma WASM | Shiki + JavaScript engine | Shiki WASM / Ferriki |
|---|---|---|---|---|
| src/highlighter.rsrust · 1396 lines · Source file | 8.46 ms | 51.7 ms | 30.1 ms | 6.1× |
| node/ferriki/src/api.mtstypescript · 321 lines · Source file | 9.99 ms | 22.5 ms | 19.9 ms | 2.2× |
| node/ferriki/src/index.mjsjavascript · 1728 lines · Source file | 37.6 ms | 203.0 ms | 137.8 ms | 5.4× |
| docs/ferriki-api.mdmarkdown · 501 lines · Source file | 4.26 ms | 12.4 ms | 15.1 ms | 2.9× |
| .github/workflows/ci.ymlyaml · 571 lines · Source file | 35.3 ms | 28.5 ms | Unsupported | 0.8× |
| node/compat/upstream/shiki/packages/twoslash/style-rich.csscss · 289 lines · Source file | 2.77 ms | 12.5 ms | 3.71 ms | 4.5× |
| node/package.jsonjson · 117 lines · Source file | 0.63 ms | 1.47 ms | 1.16 ms | 2.3× |
| Cargo.tomltoml · 118 lines · Source file | 0.51 ms | 1.17 ms | 0.85 ms | 2.3× |
| node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.pypython · 98 lines · Source file | 2.21 ms | 3.70 ms | 1.73 ms | 1.7× |
| node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.htmlhtml · 42 lines · Source file | 0.94 ms | 2.20 ms | 1.63 ms | 2.3× |
| node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.gogo · 24 lines · Source file | 0.16 ms | 0.55 ms | 0.31 ms | 3.5× |
| node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.phpphp · 46 lines · Source file | 0.35 ms | 1.82 ms | 0.91 ms | 5.2× |
| node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.shshellscript · 31 lines · Source file | 0.40 ms | 0.98 ms | 0.67 ms | 2.4× |
| node/compat/upstream/vscode-textmate/test-cases/themes/tests/test.sqlsql · 6 lines · Source file | 0.05 ms | 0.27 ms | 0.08 ms | 6.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.
| Engine | Round A | Round B |
|---|---|---|
syntect regex-onig | 0.593 ms | 0.607 ms |
syntect regex-fancy | 0.659 ms | 0.660 ms |
| Ferriki | 0.598 ms | 0.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.
| Language | syntect regex-onig | syntect regex-fancy | Ferriki |
|---|---|---|---|
| Java | 2.065 ms | 15.391 ms | 12.314 ms |
| C++ | 7.445 ms | 71.530 ms | 365.327 ms |
| CSS | 1.393 ms | 6.011 ms | 49.253 ms |
| HTML | 9.744 ms | 58.601 ms | 100.346 ms |
| JSON | 0.355 ms | 1.269 ms | 2.852 ms |
| Markdown | 3.063 ms | 58.863 ms | 118.916 ms |
| Python | 4.306 ms | 27.838 ms | 23.360 ms |
| Ruby | 2.628 ms | 20.382 ms | 83.645 ms |
| Rust | 3.544 ms | 25.590 ms | 5.585 ms |
| Shell (bash) | 2.276 ms | 25.439 ms | 10.790 ms |
| YAML | 0.855 ms | 7.369 ms | 3.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.
| Language | syntect regex-onig | syntect regex-fancy | Ferriki |
|---|---|---|---|
| Java | 0.678 ms | 1.289 ms | 0.449 ms |
| C++ | 1.123 ms | 3.016 ms | 0.890 ms |
| CSS | 0.812 ms | 1.892 ms | 0.611 ms |
| HTML | 0.886 ms | 2.034 ms | 0.891 ms |
| JSON | 0.302 ms | 0.273 ms | 0.296 ms |
| Markdown | 0.433 ms | 3.189 ms | 0.597 ms |
| Python | 1.566 ms | 3.546 ms | 0.545 ms |
| Ruby | 0.985 ms | 2.552 ms | 0.379 ms |
| Rust | 0.682 ms | 1.796 ms | 0.368 ms |
| Shell (bash) | 0.706 ms | 2.144 ms | 0.517 ms |
| YAML | 0.534 ms | 1.875 ms | 0.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.