Skip to content

perf: cut normalized JSON parsing time by over 60% - #81

Merged
RomainLanz merged 1 commit into
11.xfrom
perf/json-normalization
Sep 7, 2026
Merged

RomainLanz merged 1 commit into
11.xfrom
perf/json-normalization

Conversation

@RomainLanz

@RomainLanz RomainLanz commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Hey! 👋🏻

JSON normalization currently runs through a JSON.parse reviver. Node invokes this callback for every parsed value, even though the bodyparser only needs to transform strings. This makes normalized JSON parsing about three times slower on representative payloads.

This change keeps safeParse and its protections against __proto__ and constructor.prototype, but moves string normalization to an iterative post-parse traversal.

The traversal preserves the existing behavior for:

  • objects and arrays
  • nested values and empty keys
  • primitive roots in non-strict mode
  • disabled normalization options
  • malformed JSON and strict mode errors

It also avoids recursion during normalization.

Benchmark

Measured on Node.js 26.5.1 with the real parseJSON function and no profiler active. Each variant was warmed up before collecting 25 independent samples. The benchmark alternated the order of the variants between samples.

Payload Reviver min / median / max Post-parse min / median / max Median improvement
78,932 bytes 1,220 / 1,333 / 1,671 µs 359 / 409 / 575 µs 69.3% faster
1,064,458 bytes 15.41 / 17.93 / 22.84 ms 5.45 / 6.46 / 10.84 ms 64.0% faster

The benchmark includes request stream reading through parseText and raw-body, not only the JSON parsing step.

Compatibility

prepareJSONParserOptions now exposes an internal normalizer property instead of reviver.

This module is not part of the package export map, so public package consumers are unaffected. Applications importing the parser directly from the package sources or replacing options.reviver manually may need to update their code.

Tests

Added coverage for:

  • empty keys and their nested values
  • primitive root values in non-strict mode
  • prototype-poisoning properties
  • normalization at 10,000 levels of nesting

All 131 tests, lint, and TypeScript typechecking pass.

@thetutlage

Copy link
Copy Markdown
Member

Hey, thanks for this! I verified the numbers locally and the speedup is real.

Two things before we merge this.

  1. Add the benchmark to the repo. That way we have a historical trace and can catch regressions later.
  2. Add a nesting depth cap. This is the one behavior change. The reviver overflowed at ~5k levels and we returned a 400. Now the body gets past the parser and blows the stack inside cloneDeep in http-server, which is a 500. Please reject beyond a fixed depth with a 400, regardless of whether normalization is on.

@RomainLanz
RomainLanz merged commit 4ded265 into 11.x Sep 7, 2026
11 of 14 checks passed
@thetutlage
thetutlage deleted the perf/json-normalization branch September 7, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants