dillon1000/react

Commit

[Flight] Resolve models before JSON.stringify (#36795)

Move the toJSON handling out of the JSON.stringify replacer path and
into an explicit recursive resolution step that uses v8's optimized
single-arg JSON.stringify call.

This has been pulled out of the original implementation in
https://github.com/react/react/pull/36053 on the advice of
https://github.com/react/react/pull/36181 (thanks @unstubbable)

### NB: \_\_proto\_\_

The only difference is surrounding the treatment of `{}` vs
`Object.create(null)`. https://github.com/react/react/pull/36053 uses
the latter, which avoids `__proto__` issues, but is slower in
microbenchmarks due to v8 semantics.
https://github.com/react/react/pull/36181 uses the former (`{}`) which
is faster in micro benchmarks but doesn't special-case `__proto__`
according to spec. This PR does both (`{}` and special case), with no
measurable performance difference that I could produce.

---

The rest of this PR's description is reproduced from
https://github.com/react/react/pull/36181:

---

### Problem

When serializing a Flight chunk, `emitChunk` currently calls
`JSON.stringify(value, task.toJSON)`. The `task.toJSON` replacer is
called for every key-value pair in the serialized JSON. While the logic
inside the replacer is lightweight, the C++ to JavaScript boundary
crossing on every node adds up — V8's `JSON.stringify` is implemented in
C++, and calling back into JavaScript for every property incurs overhead
that scales with the number of keys in the output.

### Change

Replace the replacer with a two-step process:

1. `resolveModel()` recursively walks the rendered value, calling
`renderModel()` on each child — doing the same transformation the
replacer used to do, but entirely in JavaScript without C++ boundary
crossings.
2. `JSON.stringify()` is called with no replacer, staying entirely in
C++.

The `resolveModel` walk also replicates `JSON.stringify`'s `toJSON`
semantics for `Date` objects.

### Results

Measured using the Flight SSR benchmark fixture (#36180) on a dashboard
app with ~25 components, 200 product rows (~325KB Flight payload).
Tested across Node 20, 22, and 24.

- **`bench:bare`** (in-process, no script injection): Flight+Fizz sync
median improves by **~4-5%** consistently across all three Node
versions.
- **`bench:server`** (HTTP, c=1): Flight+Fizz sync throughput improves
by **~3-6%** across Node versions. Async results vary between runs but
trend positive.

### Future opportunity

While the immediate performance improvement is moderate, this change
also sets up a potential future optimization: a Flight mode that renders
to an object instead of a stream ([#36143
(comment)](https://github.com/facebook/react/issues/36143#issuecomment-4155701790)).
Since `resolveModel()` already produces a plain JS object tree before
`JSON.stringify` is called, this intermediate representation could
potentially be passed to the SSR client without the
serialization-deserialization roundtrip that the current stream-based
approach requires.

closes #36181
Browse files
Changed paths2 files
First-parent comparison
M packages/react-client/src/__tests__/ReactFlight-test.js ModifiedM packages/react-server/src/ReactFlightServer.js Modified
Patch

Files changed

Rendering syntax-highlighted changes…