Plate 06
oxc-parser vs @babel/parser: 9.1 ms vs 47.9 ms on 513 KB
Aditya Challa8 min read
I timed oxc-parser 0.153.0 against @babel/parser 7.29.9 on a 513 KB JavaScript file on this box: 9.1 ms vs 47.9 ms per parse on Node 22 with oxc's experimentalRawTransfer option (4.3× to 5.3× across two runs). With oxc's default JSON path the same file took 62.6 ms vs Babel's 47.9 ms, so the "5x faster" claim only held when raw transfer was on.
Short answer: pick oxc-parser when you are on Node 22+ and can turn on raw transfer, or when you only need the import/export record. Stay on @babel/parser when you are on Node 20, depend on Babel's AST shape (File, StringLiteral, loc), or already run Babel plugins. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 04:00 IST.
- Versions: oxc-parser 0.153.0 and @babel/parser 7.29.9 (both npm latest that day), Node 20.19.2 and Node 22.14.0, esbuild 0.28.2 for bundle sizes.
- Workload: four fixtures. A 240 B ESM file, a 15,414 B TypeScript file (trace-mapping.ts), an 18,316 B TSX file (a TanStack Router route.tsx), and a 513,377 B CommonJS-style build (Babel's own lib/index.js). oxc ran parseSync and I read result.program so the AST was actually built. Babel ran parse with sourceType module and the typescript / jsx plugins where the file needed them.
- Timing: median of 9 rounds (5,000 / 500 / 500 / 12 iterations by size), two processes per Node version with the run order flipped. Scripts and JSON in /workspace/bench-oxc-babel-parser/.
- Also measured: cold import plus first parse (11 fresh processes), esbuild bundle size, install size, error handling on a broken file, AST shape, and node counts.
- Not tested: oxc's async parse() on worker threads, experimentalLazy, the Visitor API, Babel's estree plugin, @babel/traverse or transforms, Bun and Deno, Windows or macOS bindings.
Is oxc-parser 5x faster than Babel?
Node 22.14.0, median ms per parse (run 1 / run 2):
| Fixture | oxc raw transfer | oxc default | oxc module record only | @babel/parser |
|---|---|---|---|---|
| small.mjs, 240 B | 0.0155 / 0.0104 | 0.0443 / 0.0267 | 0.0603 / 0.0323 | 0.0205 / 0.0113 |
| medium.ts, 15,414 B | 0.264 / 0.256 | 2.447 / 1.457 | 0.598 / 0.492 | 0.812 / 1.038 |
| route.tsx, 18,316 B | 0.353 / 0.238 | 1.497 / 1.476 | 0.583 / 0.588 | 1.077 / 1.284 |
| large.js, 513,377 B | 9.06 / 9.57 | 62.55 / 60.23 | 12.20 / 12.17 | 47.89 / 41.58 |
With raw transfer, oxc beat Babel by 3.1× to 4.1× on the TypeScript file, 3.1× to 5.4× on the TSX file, and 4.3× to 5.3× on the large file. Without it, oxc was slower than Babel on every fixture, because the default path serializes the AST to JSON in Rust and then runs JSON.parse in JavaScript. Raw transfer skips that step, and its AST JSON matched the default output on all four fixtures. If you are already comparing Rust-backed compilers, my swc vs esbuild vs Babel run covers the transform side; this post is only about parsing. If you only need export names from plain ESM, the lighter acorn-based route is in mlly vs acorn.
What happens on Node 20?
Node 20.19.2, median ms per parse (run 1 / run 2). Raw transfer is not available here; the option threw "not supported on ... versions of NodeJS prior to v22.0.0".
| Fixture | oxc default | oxc module record only | @babel/parser |
|---|---|---|---|
| small.mjs, 240 B | 0.0650 / 0.0527 | 0.0526 / 0.0620 | 0.0121 / 0.0208 |
| medium.ts, 15,414 B | 1.790 / 1.842 | 1.010 / 1.352 | 1.110 / 1.109 |
| route.tsx, 18,316 B | 1.866 / 1.885 | 1.039 / 0.850 | 1.231 / 1.322 |
| large.js, 513,377 B | 70.65 / 58.43 | 15.40 / 12.24 | 45.20 / 44.33 |
On Node 20, oxc's full AST was 1.3× to 1.6× slower than Babel on the large file. Reading only result.module (static imports and exports) without touching result.program came out ahead on the bigger files: about 12 to 15 ms vs 44 to 45 ms on large.js and 0.85 to 1.04 ms vs 1.23 to 1.32 ms on route.tsx. On the 15 KB TypeScript file it was mixed, and on the 240 B file Babel still won. If your CI still pins Node 20, moving to 22 is the bigger win; I compared version managers in Volta vs fnm.
Which one starts faster?
| Cold import + first parse (median of 11 processes) | oxc-parser | @babel/parser |
|---|---|---|
| Node 20.19.2 | 19.69 ms | 57.41 ms |
| Node 22.14.0 | 11.59 ms | 49.47 ms |
oxc loaded about 3× to 4× faster in a fresh process. That matters for short-lived CLIs and lint hooks, the same reason oxlint vs ESLint and Biome vs ESLint + Prettier show such large wall-clock gaps on small repos.
What does each one cost to install and bundle?
| Package | esbuild minified | gzip -9 | Install (fresh npm i) |
|---|---|---|---|
| @babel/parser 7.29.9 | 308,323 B | 78,863 B | 5,412 KB, 4 packages |
| oxc-parser 0.153.0, JS wrapper | 24,445 B | 4,567 B | 7,136 KB, 4 packages |
| oxc native binding (linux-x64-gnu .node) | 2,104,992 B | 876,249 B | included above |
oxc's JavaScript is small, but it loads a 2.1 MB native addon at runtime that esbuild cannot inline, so treat it as an external dependency when you bundle a CLI. npm on this box also pulled the musl binding next to the glibc one, which is part of the 7,136 KB. If you track package weight in CI, the setup in size-limit vs bundlesize works for both.
Do they behave the same?
| Check | oxc-parser 0.153.0 | @babel/parser 7.29.9 |
|---|---|---|
| Broken file (missing paren, dangling +) | no throw; 1 error in result.errors, empty body | threw BABEL_PARSER_SYNTAX_ERROR |
| Same file with errorRecovery: true | n/a | still threw |
| .ts file with no extra options | 0 errors; language picked from the file extension | threw unless plugins: ['typescript'] |
| Root node | Program (ESTree) | File wrapping Program |
| String literal node | Literal | StringLiteral |
| Location info | start / end offsets, no loc | start / end plus loc lines and columns |
| Nodes in route.tsx / large.js | 2,317 / 76,274 | 2,219 / 75,081 |
| Imports / exports in route.tsx | 19 / 17 via result.module | 19 ImportDeclaration / 17 Export nodes |
| Comments in route.tsx | 15 | 15 |
The AST shapes are not interchangeable. Code written against Babel node types (StringLiteral, File, loc) will need changes to read oxc's ESTree output, and the reverse is also true. Tools that already speak ESTree, such as bundlers in the Rolldown vs Rollup family, fit oxc's output more naturally.
Which one should you pick?
| Situation | My pick |
|---|---|
| Node 22+, you need the full AST fast | oxc-parser with experimentalRawTransfer |
| Node 20 and you need the full AST | @babel/parser |
| You only need imports and exports | oxc-parser, read result.module |
| You run Babel plugins or @babel/traverse | @babel/parser |
| Short-lived CLI where startup dominates | oxc-parser (11.6 vs 49.5 ms cold on Node 22) |
| You need errors returned, not thrown | oxc-parser |
| You bundle the parser into a single JS file | @babel/parser (no native addon) |
Bottom line: on this box oxc-parser 0.153.0 parsed a 513 KB file in about 9 ms vs Babel's 42 to 48 ms, but only on Node 22 with raw transfer on. On its default path, or on Node 20, Babel was faster for full ASTs. Who should not switch yet: teams on Node 20, anyone who depends on Babel's node types, and anyone shipping a single-file bundle. Who should try oxc now: Node 22 tool authors who parse many files and can handle ESTree. For the loader side of a TypeScript toolchain, see tsx vs ts-node.
How this was made: I installed both parsers on the ShopperCove box, timed four fixtures over 9 rounds in two processes per Node version (20.19.2 and 22.14.0), checked cold start in 11 fresh processes, bundled each entry with esbuild, ran a fresh npm install for size, and compared error handling and AST output. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/oxc-project/oxc/tree/main/napi/parser
- https://oxc.rs/docs/guide/usage/parser.html
- https://www.npmjs.com/package/oxc-parser
- https://babeljs.io/docs/babel-parser
- https://www.npmjs.com/package/@babel/parser
- https://github.com/babel/babel/tree/main/packages/babel-parser
Related
- https://www.shoppercove.com/blog/swc-vs-esbuild-babel
- https://www.shoppercove.com/blog/oxlint-vs-eslint
- https://www.shoppercove.com/blog/biome-vs-eslint-prettier
- https://www.shoppercove.com/blog/rolldown-vs-rollup
- https://www.shoppercove.com/blog/volta-vs-fnm
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/tsx-vs-ts-node
- https://www.shoppercove.com/blog/mlly-vs-acorn
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~04:00 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2 and 22.14.0). oxc-parser 0.153.0 vs @babel/parser 7.29.9; esbuild 0.28.2. Fixtures 240 B mjs / 15,414 B ts / 18,316 B tsx / 513,377 B js. Median of 9 rounds x 5,000/500/500/12, 2 processes each Node. Node 22 raw transfer: 0.0155/0.0104, 0.264/0.256, 0.353/0.238, 9.06/9.57 vs Babel 0.0205/0.0113, 0.812/1.038, 1.077/1.284, 47.89/41.58. Default oxc slower than Babel on every fixture (large 62.55/60.23 Node 22; 70.65/58.43 vs 45.20/44.33 Node 20). Module-record-only large 12.2 ms. Raw AST JSON identical to default on all 4. Cold 19.69 vs 57.41 ms (N20), 11.59 vs 49.47 (N22). Behavior: oxc returned 1 error on broken file, Babel threw (errorRecovery too); Babel needs typescript plugin; ESTree Literal vs StringLiteral/File/loc. Not tested: async parse, lazy, Visitor, Bun/Deno. No affiliate.