Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ECMAScript Parser Benchmark (npm)

Benchmarks for ECMAScript parsers available as npm packages, including pure JavaScript parsers and native parsers (Zig, Rust) via NAPI bindings.

System

Property Value
OS macOS 24.6.0 (arm64)
CPU Apple M3
Cores 8
Memory 16 GB

Parsers

A tiny, fast JavaScript parser, written completely in JavaScript.

A JavaScript compiler and parser used by the Babel toolchain.

A high-performance JavaScript and TypeScript parser written in Rust.

An extensible Rust-based platform for compiling and bundling JavaScript and TypeScript.

A high-performance & spec-compliant JavaScript/TypeScript compiler written in Zig.

Benchmarks

File size: 7.83 MB

Bar chart comparing npm parser speeds for typescript.js

Parser Median RME Mean Min Max Ops/sec Relative
Yuku 46.06 ms ±0.55% 46.39 ms 43.71 ms 70.85 ms 21.71 ops/s baseline
Acorn 138.05 ms ±1.23% 138.32 ms 124.36 ms 156.35 ms 7.24 ops/s 3.00× slower
Babel 188.32 ms ±3.56% 191.29 ms 143.77 ms 318.37 ms 5.31 ops/s 4.09× slower
Oxc 263.65 ms ±0.24% 263.50 ms 257.55 ms 328.30 ms 3.79 ops/s 5.72× slower
SWC 508.10 ms ±2.84% 529.76 ms 466.83 ms 907.39 ms 1.97 ops/s 11.03× slower

File size: 2.95 MB

Bar chart comparing npm parser speeds for checker.ts

Parser Median RME Mean Min Max Ops/sec Relative
Yuku 16.80 ms ±0.61% 17.20 ms 15.71 ms 27.98 ms 59.54 ops/s baseline
Babel 80.02 ms ±1.44% 80.11 ms 62.15 ms 99.54 ms 12.50 ops/s 4.76× slower
Oxc 81.69 ms ±0.55% 82.53 ms 79.70 ms 105.54 ms 12.24 ops/s 4.86× slower
SWC 151.90 ms ±0.38% 152.58 ms 149.27 ms 199.76 ms 6.58 ops/s 9.04× slower
Acorn Failed to parse - - - - - -

File size: 0.07 MB

Bar chart comparing npm parser speeds for react.js

Parser Median RME Mean Min Max Ops/sec Relative
Yuku 0.30 ms ±0.30% 0.31 ms 0.28 ms 5.24 ms 3372.20 ops/s baseline
Acorn 0.88 ms ±0.22% 0.90 ms 0.83 ms 4.30 ms 1133.17 ops/s 2.98× slower
Babel 1.35 ms ±0.69% 1.43 ms 0.98 ms 8.39 ms 739.74 ops/s 4.56× slower
Oxc 1.50 ms ±0.23% 1.53 ms 1.47 ms 7.23 ms 665.89 ops/s 5.06× slower
SWC 2.78 ms ±0.32% 2.88 ms 2.72 ms 22.69 ms 359.16 ops/s 9.39× slower

Run Benchmarks

Prerequisites

  • Bun - JavaScript runtime and package manager

Steps

  1. Clone the repository:
git clone https://github.com/yuku-toolchain/ecmascript-parser-benchmark-js.git
cd ecmascript-parser-benchmark-js
  1. Install dependencies:
bun install
  1. Run benchmarks:
bun bench

This will run benchmarks on all test files. Results are saved to the result/ directory.

Benchmark duration is configurable via the environment variables BENCH_TIME (timed duration per run in ms, default 10000), BENCH_WARMUP (warmup duration in ms, default 2000), and BENCH_RUNS (independent runs per parser, default 3). For the most stable numbers, run on AC power with no other applications running.

Methodology

Each parser is benchmarked using Tinybench with warmup iterations followed by multiple timed runs. Each run measures the time to parse the source text into an AST. Source files are read from disk once and kept in memory for all iterations.

To keep results stable and fair, every parser × file combination runs in its own freshly spawned process, so JIT state and GC pressure from one parser never affect another. Each combination is benchmarked in multiple independent runs (3 by default), and the reported median is the median across those runs, a statistic that is robust to GC pauses, OS scheduling blips, and other outliers. The RME column shows the relative margin of error (99% confidence) within a run. Differences between parsers smaller than their combined margins should be treated as noise.

Native parsers (Oxc, SWC, Yuku) run through their respective NAPI bindings, so measured time includes the binding overhead. Pure JS parsers (Acorn, Babel) run directly in the JavaScript runtime.

Why is Oxc slower than Babel here? By default, oxc-parser serializes the AST to a JSON string on the Rust side and runs JSON.parse on the JavaScript side when you access result.program. Oxc's Rust-side parsing is extremely fast. It is this serialization boundary that dominates the end-to-end time. (If you call parseSync and never touch the result, Oxc looks much faster, because program is a lazy getter that defers the JSON.parse. The benchmarks above measure the time to actually obtain the full AST, which is what any real consumer of a parser does.)

What about Oxc's experimentalRawTransfer? Oxc also has a hidden experimental path that removes the JSON step entirely. It is not part of Oxc's documented parser options, but when the flag is passed, Rust parses directly into a huge ArrayBuffer shared with JavaScript, and generated JS code deserializes AST nodes straight out of that memory. It is genuinely clever engineering, and with it enabled Oxc's end-to-end numbers land much closer to Yuku's, since the two pipelines become architecturally the same (binary buffer in, ESTree objects out).

It is not part of the results above for a simple reason. This benchmark measures each parser's default, documented, stable API, the code path every user gets from npm install. Raw transfer today is an undocumented flag that is none of those things, and it cannot run here at all.

  • It does not work on Bun, which this benchmark runs on. oxc-parser's own rawTransferSupported() check returns false on Bun, and on Node < 22 and Deno < 2, because the design requires allocating an ArrayBuffer larger than 4 GiB. Enabling the option in this harness would simply throw.
  • It needs gigabytes of reserved memory per parse. The trick that makes it fast is that the Rust arena is a fixed 2 GiB block aligned to a 4 GiB boundary, so 64-bit Rust pointers can be read from JavaScript as 32-bit buffer offsets. Achieving that alignment means reserving ~6 GiB of address space per buffer. Most of it stays virtual until touched, but every concurrent parse still needs its own dedicated 2 GiB arena, source and AST together must fit inside that block, and only 64-bit little-endian platforms are supported.

If Oxc stabilizes raw transfer as its default path, we will happily benchmark it. That is the fair comparison we want.

Why is Yuku fast without any of that? Yuku reaches the same zero-serialization end state, but by construction rather than by workaround. Its Zig parser does not build a pointer graph that later needs remapping. The AST is already binary, a flat table of fixed-size 44-byte node records addressed by index, with strings stored as offsets into the source. That layout is its own transfer format. It crosses the NAPI boundary as one small buffer sized to the actual AST, and a generated decoder reads it in JavaScript through typed arrays, conceptually the same read side as Oxc's raw transfer minus the pointer-to-offset machinery that forces the giant fixed arena.

Because there are no pointers to remap, there is no 2 GiB block, no 4 GiB alignment requirement, no multi-gigabyte reservation, and no runtime gate. The same code path runs on Node, Bun, and Deno, parses files in parallel without multiplying memory, and uses only the memory the file actually needs. Zig's comptime generates both the binary layout and the JavaScript decoder from one source of truth, so they can never drift apart. The numbers in the tables above are Yuku's default and only mode. There is no faster experimental path being held back and nothing extra to enable.

About

Benchmarks for ECMAScript parsers available as npm packages, including pure JavaScript parsers and native parsers (Zig, Rust) via NAPI bindings.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages