Files
react/packages/react-client
Michael HartandGitHub ad37e01155 [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
2026-06-16 10:01:50 +02:00
..
2020-03-07 11:23:30 -08:00

react-client

This is an experimental package for consuming custom React streaming models.

Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.

Use it at your own risk.