Analysis

Removing Cow<str> speeds Rust JSON formatter 42%

A single `Cow<str>` was hiding real work in JJPWRGEM, and deleting it helped unlock a 42% faster JSON path.

Sam Ortega··5 min read
Published
Listen to this article0:00 min
Share this article:
Removing Cow<str> speeds Rust JSON formatter 42%
Source: pexels.com

The fastest move in this Rust JSON formatter was not a clever algorithm. It was removing a type that looked flexible on paper but kept dragging the hot path through extra token handling and state-machine work, and that cleanup ended up delivering a 42% combined benchmark gain for JJPWRGEM.

The surprising lesson: less abstraction, more speed

Viacheslav Biriukov’s write-up makes the kind of performance point Rust developers learn the hard way: a generic-looking design can cost real CPU time when it sits in the inner loop. In JJPWRGEM, the old baseline represented numbers as a single `Cow<str>`, which made the code feel ergonomic but also forced the formatter to carry around borrowed-versus-owned ambiguity in a place where it wanted to move fast.

That choice added avoidable overhead in two places that matter a lot for a formatter: token plumbing and the state machine that decides what to emit next. Once that path was simplified, the CPU had less bookkeeping to do, and the formatter could spend its time on the actual work of normalizing and writing JSON instead of shuffling abstractions.

What changed in the number pipeline

The headline gain was not just from dropping `Cow<str>`. The 42% figure refers to the combined benchmark result after number normalization, which is where the real clean-up happened. The formatter now lowercases exponent symbols, strips plus signs, and removes zero exponents, so a number like one with redundant formatting gets rewritten into a simpler canonical form.

That matters because the old setup made every number pay for flexibility even when the output rules were straightforward. By handling normalization directly, JJPWRGEM cut out extra branches and state transitions without giving up the ability to format real JSON faithfully. This is the part that makes the result feel more like engineering than trickery: the code got simpler at the exact spot where the machine was doing the most repeated work.

The benchmark context was already brutal

The performance gap was not happening in a toy benchmark. On a 2 MB JSON file, Prettier and Oxfmt both took more than a second, while JJPWRGEM was around 30 milliseconds on the same kind of test. That is an enormous gap, and it explains why a 42% improvement inside an already fast formatter is worth caring about.

The author also notes that Oxfmt is currently 10 to 20% slower than Prettier for JSON, even though Oxfmt has made major gains for JavaScript formatting. One report found a migration to Oxfmt made a repository format 6.5x faster, dropping from 13.9 seconds to 2.1 seconds, which is a good reminder that wins in one workload do not automatically transfer to another. JSON has its own pressure points, and this story is about finding the one that mattered most here.

Why the gain looks believable across real inputs

This was not a single narrow benchmark cherry-picked to flatter the new code. The post says the benchmarks covered multiple JSON shapes, including dense floats, many objects and integers, and strings with deep nesting. That spread is important because a normalization pass can look great on one kind of input and fall apart on another.

Here, the numbers suggest the improvement came from reducing work in the formatter’s core path rather than gaming one data shape. That is also why the author frames the change as well-scoped: it improved the current formatter while setting up future feature work, which is exactly what you want from a performance refactor. You are not just shaving cycles, you are making the next change easier to fit into the same pipeline.

How to spot the same trap in your own Rust code

The lesson for Rust code is not that `Cow<str>` is bad. It is that any zero-copy or generic abstraction has to earn its keep in the hot path, especially when it pulls tokenization, normalization, or state tracking into the design. If the abstraction survives only because it feels elegant, measure it twice.

When you are looking for this kind of slowdown, check for the same warning signs:

  • A borrowed-or-owned type crosses a hot boundary, but the code immediately normalizes or rewrites the data anyway.
  • A parser or formatter keeps extra state only to preserve flexibility that the caller never uses.
  • The code has more branches for shape management than for actual output work.
  • The benchmark gets slower on common data shapes, not just on pathological ones.
  • Removing a wrapper type makes the code easier to read and easier for the compiler to optimize at the same time.

The trick is to ask whether the abstraction is saving work or just moving it around. In JJPWRGEM, `Cow<str>` was doing the second one, and once number normalization was rewritten to match the formatter’s real job, the improvement landed where it counted.

The Rust community noticed because the story is practical, not theoretical

The post later turned up in This Week in Rust 651 and drew discussion on Lobsters, which makes sense for a story that is both specific and easy to generalize. Rust developers know the temptation to reach for flexible types early, and they also know how often the best performance wins come from deleting a layer instead of adding one.

That is why this one sticks: JJPWRGEM was already fast enough to embarrass big-name tools on a 2 MB JSON file, yet the best improvement still came from removing unnecessary abstraction in the number path. The opening lesson holds all the way through the benchmark data and the later community discussion: in Rust, the cleanest speedup is often the one that makes the machine do less thinking.

Know something we missed? Have a correction or additional information?

Submit a Tip

Never miss a story.

Get Rust Programming updates weekly. The top stories delivered to your inbox.

Free forever · Unsubscribe anytime

Discussion

More Rust Programming News