I Wrote Multiple CUE Parsers and Benchmarked Them Against JSON
It started yesterday because I wanted to fix JSON.
Well, not fix exactly. More like figure out if there was something better for client-side parsing in the browser. I’d been looking at comparing JSON versus MessagePack when I stumbled into CUE, a configuration language that embeds schema definitions directly in the data.
I posted about it and then was like well what if I just actually did it. There was just one problem: the core CUE spec is written in Go, which obviously doesn’t help when you need something running in React.
So I wrote a TypeScript parser for CUE. You can find it at cue-ts.
Then I built a benchmark app with 11 different parsing and deserialization approaches, all running client-side in a Vite + React app. We’ll see how that turned out here.
The Contenders
I tested across three ecosystems:
JSON: Native JSON.parse, JSON + Zod validation, and JSON + Ajv (both pre-compiled and per-call schema compilation).
CUE: Full AST parsing, a TypeScript deserializer, a fused single-pass scanner, pre-compiled CUE schemas, and even a Rust-to-WASM CUE parser.
Binary: MessagePack decoding, with and without Zod validation.
Payloads ranged from ~1KB to ~460KB across CUE, JSON, and MessagePack formats.
Understanding the Schema Problem
Before we look at numbers, we need to talk about schema validation.
With Zod, your schema is TypeScript code. It’s set at compile time, so there’s no runtime schema parsing overhead. With Ajv in compiled mode, it takes a JSON Schema document and generates an optimized JavaScript validation function. You do this once, and the runtime cost is essentially zero.
But CUE does something different. It embeds schema definitions directly in the data file:
#User: {
email: string & =~"^[^@]+@[^@]+$"
role: "admin" | "editor" | "viewer"
age: int & >=0
}
user: #User & {
email: "[email protected]"
role: "admin"
age: 30
}
Schema and data together, processed in one pass. Sounds elegant, right? The question is whether that elegance costs you speed. 😅
The Results
Here’s the 10KB payload benchmark on Chromium/V8:
| Strategy | Median | vs JSON.parse |
|---|---|---|
| JSON + Ajv (compiled) | 18 μs | ~equivalent |
| JSON.parse | 18 μs | baseline |
| JSON + Zod | 28 μs | 1.6x slower |
| MsgPack Decode | 30 μs | 1.7x slower |
| CUE (compiled schema) | 112 μs | 6.2x slower |
| CUE Fast Deserialize | 114 μs | 6.3x slower |
| CUE Deserialize (WASM) | 755 μs | 41.9x slower |
JSON + Ajv compiled is equivalent to bare JSON.parse. The pre-compiled validator adds essentially zero overhead. Case closed?
Not quite.
The Fair Fight
Those benchmarks aren’t comparing the same thing. Zod and Ajv compiled both pre-process their schemas before the benchmark runs. CUE processes its schema on every call. So I ran the fair comparison, schema processing included:
| Strategy | Median |
|---|---|
| CUE Fast Deserialize | 114 μs |
| CUE (compiled schema) | 112 μs |
| JSON + Ajv (interpret) | 4,840 μs |
When JSON has to compile its schema per call, CUE is 43x faster. Cool? All that work really paid off?
Well, the bottleneck for CUE isn’t schema processing. It’s parsing the text format itself. I tried basically every optimization I could think of. The deserializer spends nearly all its time scanning characters, building strings and numbers. Without native C++ code, which is what JSON.parse gets for free from the browser engine, a TypeScript parser just can’t close that gap.
MessagePack: Not Worth It
I included MessagePack benchmarks to see if smaller binary payloads would translate to faster parsing. They don’t. MessagePack is consistently slower than JSON.parse, even with smaller payloads. The overhead of maintaining a separate binary serialization format on the client side just isn’t worth it for most use cases.
Cross-Browser Highlights
I ran a few tests across engines. Some notable findings:
- Safari (JSC): Zod validation is essentially free. JSON + Zod matches bare
JSON.parse - Chrome (V8): CUE’s fast deserializer benefits from
charCodeAtoptimizations - Firefox (SpiderMonkey): Most consistent results across the board
So What’s the Answer?
If you can compile your schema ahead of time with Ajv or Zod, do that and use JSON.
It’s not even close. Seriously.
JSON.parse benefits from decades of native browser optimization that no TypeScript parser can match, and pre-compiled validation adds essentially zero overhead.
CUE is faster in one specific scenario: when you need to process schemas dynamically on every call. 43x faster than per-call Ajv compilation is real, but how often does that actually come up in production? It’s hard to say.
| Use Case | Pick This | Why |
|---|---|---|
| Hot-path APIs | JSON + Ajv (compiled) | Fastest possible |
| TypeScript projects | JSON + Zod | Best DX |
| Dynamic schemas | CUE | 43x faster than per-call Ajv |
| Config files | CUE | Single source of truth |
| Bandwidth-constrained | MsgPack | 30-60% smaller payloads |
One thing I considered but didn’t build for this post: a conversion layer that stores everything as CUE on the server, then compiles it down to JSON with Zod or Ajv schemas for the browser. You’d get CUE’s authoring experience with JSON’s runtime speed. That feels like an interesting project, and maybe I’ll get to it next.
Both the cue-ts parser and the benchmark app are open source. Try them yourself and let me know what you think.
/ Programming / Webdev / javascript / Benchmarks