402 Commits
Author SHA1 Message Date
Hendrik Liebau f8e63c6645 [Flight] Preserve leading U+FEFF in text rows (#37625)
The Flight client dropped a leading `U+FEFF` from outlined text rows
when it decoded binary input. `TextDecoder` consumed the character as an
encoding signature, even though it was part of the serialized string.
Both the Node and Web decoders now use `ignoreBOM: true` to preserve it.

Regression tests cover one and two leading U+FEFF characters with normal
chunks and with every UTF-8 byte delivered separately. They also include
an inline-string control. The global `TextDecoder` Flow declaration now
makes `fatal` optional and declares `ignoreBOM`, matching the API.
2026-09-22 17:07:13 +02:00
Andrew Clark 71f7255937 [Flight] Server References for arbitrary object types (#37636)
Added behind a new experimental flag, `enableFlightObjectReferences`.

Extends Server References so that a reference can point to an object,
not just a function. An object is registered with a new API,
`registerServerObjectReference`, which tags it with its module id the
same way `registerServerReference` tags a Server Function.

The client receives an opaque handle: reading a property or calling it
throws, similar to how Temporary References work inside Server
Functions. The only thing the client can do with the handle is pass it
back to the server via a Server Function, where it resolves through the
server manifest.

The motivating use case is letting a framework pass request-scoped
values by reference, so it can omit them from the request body. For
example, a framework could model `searchParams` as a module export whose
value resolves from the current request. This avoids encoding the search
params twice (they already appear in the request URL) but it also makes
the responses more cacheable: if the search params don't appear
elsewhere in the body, then a response cache can omit them from its
cache key.

This initial PR only exposes the new API in the Turbopack bindings. We
will port it to the other bundler configs if the experiment advances.
2026-09-17 12:27:10 -04:00
Hendrik Liebau 6c0e1047e3 [Flight] Keep an element pending while a referenced row is resolving (#37542)
A row that is still parsing can hand its partially built value to
references that were registered on it during that parse.
`initializeModelChunk` fulfilled every such listener, on the assumption
that all of them are cyclic references back into the parsing row. Only
some are. A listener from a nested parse that is not part of a cycle
belongs to a handler that does not wait on the parsing row, so
fulfilling it early completes that handler with an object that still has
references outstanding.

When that handler owns an element, `initializeElement` runs on
incomplete props. In DEV the props are frozen, so the write that arrives
later throws `Cannot assign to read only property`, and
`rejectReference` escalates the error into the rows that wait on the
element, up to the root. Only the debug tree can produce this shape. The
RSC stream writes element props inline, while the debug channel outlines
a props object that an element shares with its own componentInfo into a
separate row, and that row can still wait on a client module.

`resolveBlockedCycle` already tells a cyclic reference from any other,
but it returned `null` for mid-parse listeners because `handler.chunk`
was assigned after the drain loop. This change assigns it before the
loop and classifies each listener. A reference whose handler is
transitively waiting on the parsing row is a genuine cycle and receives
the value now, because neither side can complete before the other. Every
other listener is queued back on the parsing row and fulfilled when that
row completes, like any reference into a blocked row.

A row whose own parse fails used to hand the partial value to its
mid-parse listeners before it threw. It now errors through
`triggerErrorOnChunk`, which rejects them the same way a reference into
any other errored row is rejected. The `if (handler.errored) throw`
after the loop is removed. It also caught a rejection during the loop,
which now reaches `triggerErrorOnChunk` on its own because
`handler.chunk` is set.

One behaviour changes beyond the reported bug. When
`initializeDebugChunk` errors a chunk before `parseModel` runs, the old
code set `INITIALIZED` over that status if the model had no pending
references, and left it `ERRORED` otherwise. The chunk now stays
`ERRORED` in both cases. That is what the `triggerErrorOnChunk` call in
`initializeDebugChunk` intends, and the TODO above the `parseModel` call
already notes that the chunk can be `ERRORED` there.

PR #37398 deferred `Object.freeze(element.props)` until the outstanding
references have resolved. That removes the exception but not the cause:
the element is still initialized on an incomplete object and is visible
through `_debugInfo` with a `null` placeholder until the late write
lands. With the early release fixed, the freeze needs no change.

**Alternatives Considered**

- Deferring every listener whose handler is not the parsing row's own
deadlocks `foo ↔ bar` in `can deduped outlined references inside
promises`. One side of a genuine cycle has to accept the partial object.
- Holding an element back while its props row is `BLOCKED` breaks
`should handle deduped props of re-used elements in fragments`, where
the row is blocked on an unrelated module and the props object itself is
complete.
- A per-object count of pending writes plus a reverse `dependents` edge
works, but adds a second dependency graph next to `deps` and
special-cases elements.

Fixes #37361
Closes #37398
2026-09-08 13:31:40 +02:00
Hendrik Liebauandsundeep8967 8f0043721e [Flight] Fix RangeError from exponential debug info growth (#37481)
The Flight Client copies the debug info of a referenced chunk into the
chunk that references it, so that the receiving chunk records what
blocked it. It copies the entries once per reference, and a referenced
chunk already carries the entries that it received itself. A response
that deduplicates the same object across a chain of rows therefore
multiplies the entries at every step. In development the array
eventually grows past what the engine can allocate for it, and the
client throws `RangeError: Invalid array length`.

The receiving chunk now takes each entry only once. The entries are
copied by reference and never cloned on this path, so a comparison by
identity is exact. The array becomes bounded by the number of distinct
entries in the response rather than by a chosen limit.

The bookkeeping costs one `Set` per chunk that receives debug info, in
development only. The set holds a reference to each entry rather than a
copy, so the entries stay shared and nothing about the debug info is
duplicated. A chunk receives entries only while it is blocked, so the
fix releases the set as soon as the chunk initializes. Debug info still
accumulates transitively, which this change does not alter.

This change also adds the `!reference.isDebug` guards that #37358
proposes for the element props branch and the default branch of
`fulfillReference`. #35795 introduced the rule that a reference resolved
during debug info resolution does not transfer, and it left those two
branches behind. The guards make that rule hold at every branch.
However, those branches reference debug chunks that carry no entries, so
the guards change no observed behaviour, and they do not fix the growth
in #37343, which comes from references in model chunks. #37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because #35795 already skips it there.

The rest of #37358 deduplicates the entries, which is the right
direction, but it scans the receiving array for every candidate, which
is quadratic in the size of the debug info. #37359 caps the array at a
constant instead, which stops the crash but keeps copying the duplicates
and drops debug info once a response passes the cap.

**Alternatives Considered**

- Tracking the referenced chunks rather than the entries would be
cheaper, because it would need one map entry per referenced chunk. It
would not be enough, because a chunk can reach the same entry through
two paths. A chunk can hold a client reference directly and also
reference a chunk that already received the debug info of that client
reference.
- Recording the last receiving chunk on each entry would be exact while
a chunk parses its model, where the transfers into it are consecutive.
It would break once transfers into different chunks interleave, and that
is the path the reported crash takes.
- Turning `_debugInfo` itself into a `Set` is not possible, because the
reconciler, Fizz, the Flight Server and DevTools read it by index and
depend on its order.

Fixes #37343
Closes #37358
Closes #37359

Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
2026-09-02 15:57:40 +02:00
Sebastian "Sebbie" SilbermannandClaude Code 3c397fe760 [test] Stop leaking mocked performance clocks into later test files (#37379)
Test files running in the Node.js Jest environment share the worker
process's `performance` object, because [`jest-environment-node`
installs it by
reference](https://github.com/jestjs/jest/blob/v29.7.0/packages/jest-environment-node/src/index.ts#L85-L103)
rather than by copy. When a test mocked the clock with
`Object.defineProperty(performance, 'now', ...)`, the mutation hit the
shared object and was never undone, since Jest only restores
`jest.spyOn` mocks when a file's runtime is torn down ([`jest-runtime`'s
`teardown()` calls
`restoreAllMocks()`](https://github.com/jestjs/jest/blob/v29.7.0/packages/jest-runtime/src/index.ts#L1358-L1359)).
Every subsequent test file in the same worker then observed the fake
clock, including jsdom-based files, whose [`performance.now()` subtracts
a window-creation timestamp from the shared object's
`now()`](https://github.com/jsdom/jsdom/blob/v22.1.0/lib/jsdom/living/hr-time/Performance-impl.js#L13-L14).

This change switches the six affected test files to
`jest.spyOn(performance, 'now')` and `jest.spyOn(performance,
'timeOrigin', 'get')`, which Jest restores automatically at teardown.
`ReactFlightDOMEdge-test.js` runs in jsdom and therefore did not leak,
but it used the same pattern and is converted for consistency.

This change is mostly for test hygiene. Was discovered while
investigating a flaky `{"time":NaN}` serialisation bug (e.g.
https://github.com/react/react/actions/runs/32878999825/job/97903912119)

Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
2026-08-26 12:49:48 +02:00
danandClaude Opus 5 dc631ef588 [Flight] Outline and dedupe repeated strings (#37147)
Flight has two ways to write a string:

1. Small ones are inlined into the JSON model.
2. Large ones (>= 1024 chars) are outlined into a binary text row so
they don't get double-encoded and double-parsed.

Neither is ever deduplicated. That's most visible in client reference
metadata, where a route repeats the same bundler chunk URLs across every
client reference (for example,
https://github.com/vercel/next.js/issues/95559).

**This PR adds a dedupe map for strings inside import metadata.** A
string is written once into its own row, and every occurrence is a
reference to it.

## How it works

When we're about to write a string in import metadata at least as long
as the threshold, we look it up in the request's map:

- **If it's not in the map:** Emit a row containing the string, store
that row's reference in the map, and write the reference here.
- **If it is:** Write the reference.

So every string goes on the wire once, and every occurrence costs a few
bytes. An earlier version waited for the second occurrence before
outlining, which is the right default for arbitrary strings where most
never repeat (it's what #27537 does for objects). Import metadata is the
opposite case: a chunk is listed by every client module that lives in
it, so a chunk name that appears once is the exception. In the bench
app's three routes, every chunk string at least 16 characters long
appears more than 20 times and none appears once. Outlining on first
sight saves the inline copy, and a string that never repeats costs 7
bytes more than inlining it.

Import metadata needs its own map and its own queue. The client resolves
a client reference as soon as it parses the import row, and import
chunks flush ahead of model rows, so the string row has to be in the
same queue to arrive first.

The client needs no protocol changes. It already resolves `$N`
references, and a row holding a string resolves to that string. It does
get a check that import metadata never blocks on a row that hasn't
arrived, which is the other end of the queue ordering above, and
`getOutlinedModel` stops allocating a path array for references without
one.

Model strings are left alone. An earlier version of this PR deduped them
too, but we're not going to do this now per review (maybe in a
follow-up). Metadata on the debug channel is also left alone: it's a
separate serialization path, and deduping across the two would make the
main payload depend on whether a debug channel is attached.

## Threshold

The trigger is 16, low compared to what a model-side threshold would
want, because import metadata is repetitive but its parts are short. How
much this saves depends on how many client references share a chunk
list, so measuring one string on its own is misleading. For a chunk path
of realistic length today:

| references sharing the chunk | before | after |
|---|---|---|
| 1 | 80 B | 87 B |
| 2 | 160 B | 122 B |
| 3 | 240 B | 157 B |
| 5 | 401 B | 228 B |
| 10 | 813 B | 416 B |
| 40 | 3273 B | 1526 B |
| 80 | 6594 B | 3047 B |

(Import and string rows only.) It costs 7 bytes at 1 reference and wins
from 2. Chunk paths in this app are 47 characters, so a threshold of 48
or higher saves nothing at all here. That's why it's 16: picking a
number just under one bundler's path length gives you something that
quietly stops working on the next bundler.

The map is bounded by the combined length of the strings it holds, 32
KiB. Once the budget is spent, new strings are written inline every time
while strings already outlined keep deduping. That makes the savings
depend on the order strings are first seen: a shared chunk URL first
encountered after 32 KiB of unique module ids won't be deduped. That's
main's behavior, so it's a missed win rather than a regression, but a
manifest-heavy dev route could hit it.

## Byte measurements

Three routes of a Next.js app, serial requests:

| route | Flight | document | document (gzip) |
|---|---|---|---|
| `/dashboard` | −48.4% (710.1 → 366.5 KB) | −34.3% | −7.8% |
| `/docs` | −5.8% (555.2 → 523.1 KB) | −5.0% | −0.7% |
| `/blog` | −5.4% (878.8 → 831.7 KB) | −4.4% | −1.7% |

The difference between the routes is how many client references each one
has. On `/dashboard` the import rows shrink from 388.0 KB to about 32 KB
with the row *count* unchanged at 114, because every client reference
repeats the same 49 chunk URLs.

gzip already collapses repeated strings, so −48.4% raw is only −7.8%
compressed. The bytes still have to be escaped, encoded and copied
before they reach the compressor, which is where most of the speedup
below comes from.

## Speed measurements

Benchmarked end-to-end through a Next.js app on Vercel Sandbox VMs (x86
Xeon), 16 boots, paired ABBA within each boot, boot as the unit of
replication. Base is the merge-base with main, `eafeac09`; candidate is
the current head, `e0b4614c`.

| cell | effect | 95% CI | p |
|---|---|---|---|
| `/dashboard` serial req/s | **+16.9%** | ±1.7 | <0.0001 |
| `/dashboard` serial p95 latency | −18.2% | ±2.4 | <0.0001 |
| `/dashboard` serial TTFB | −23.2% | ±1.2 | <0.0001 |
| `/dashboard` under load req/s | **+17.0%** | ±3.4 | <0.0001 |
| `/dashboard` under load median latency | −13.9% | ±2.5 | <0.0001 |
| `/docs` serial req/s | +3.0% | ±1.4 | 0.0003 |
| `/docs` serial TTFB | −3.1% | ±1.0 | <0.0001 |
| `/blog` serial req/s | +2.4% | ±1.0 | 0.0001 |
| `/blog` serial median latency | −2.4% | ±0.7 | <0.0001 |

No detected difference: `/blog` and `/docs` under load (p=0.13–0.56).
All 16 boots are positive on both `/dashboard` cells. The `/dashboard`
headline has now been measured in four separate 16-boot runs across four
heads of this branch and is p<0.0001 in each; the small routes cleared
p<0.01 only on this head, after the serializer change below, having sat
at p=0.02–0.06 on the three earlier heads.

The previous head, `4569e1d6`, which outlined on the second occurrence
rather than the first, measured +14.9% ±2.3 on `/dashboard` serial req/s
and −17.3% ±5.7 on TTFB against the same base. Those intervals overlap
the ones above, so the switch is not a measurable speedup on its own;
the bytes it saves are about 1% of the payload.

In a real browser on `/dashboard` (measured on an earlier commit of this
branch, `aed4d523`), hydration is −2.8% ±1.3 (p=0.0003) / −2.4% ±0.9
(p=0.0001) and LCP is −5.3% ±2.0 (p<0.0001) / −3.6% ±2.6 (p=0.009).
Client navigation is under the noise floor in both.

### Where the time goes

32 CPU profiles, taken after the timed runs with an identical request
count in both arms, so absolute sampled milliseconds are comparable. One
pass per boot, no replication statistics — directional, not a claim.
These profiles are from `aed4d523`. The current head also walks the
metadata into a copy before a plain `stringify`, after a detour through
a `stringify` replacer that measured 2.3× slower in isolation (a
replacer function takes V8 off its fast path for the whole call); the
`transformImportMetadata` frame below is a fair proxy for the current
cost.

Cheaper:

| base | candidate | frame |
| ---: | ---: | --- |
| 23.6 s | 5.7 s | ReactDOM `preinitScript` |
| 24.9 s | 10.6 s | `serializeClientReference` |
| 81.8 s | 67.3 s | `utf8Write` |
| 93.9 s | 80.8 s | `createFromString` |
| 50.7 s | 39.2 s | Next's `htmlEscapeJsonString` |

More expensive:

| base | candidate | frame |
| ---: | ---: | --- |
| 0 | 9.3 s | `transformImportMetadata` |
| 2.0 s | 10.3 s | `getOutlinedModel` (SSR-side Flight client) |
| 7.7 s | 11.7 s | `parseModelString` |
| 154.8 s | 158.2 s | `resolveModelToJSON` |

About +31 s of new work against −79 s inside the runtime bundle and −45
s in node's buffer and string layer.

`getOutlinedModel` resolving references is the mechanism working, not a
warning sign. Next.js runs a Flight client on the server to read its own
payload, and a `/dashboard` payload goes from 0 references inside import
rows to 4964, so a frame that barely ran before now runs once per
reference. Each call is a lookup on a row that has already been
initialized: the string row goes into the import queue ahead of the
import row that reads it, so it has always arrived and nothing blocks.
`parseModelString` grows for the same reason.

`preinitScript` doesn't get cheaper from writing fewer bytes. It does
two dictionary lookups keyed by the chunk URL per call, and the call
count and argument values are unchanged — the resolved models are
identical. What changes is string identity: in the base build every one
of the 5013 chunk-URL occurrences is a fresh string out of `JSON.parse`
whose hash has to be computed before the lookup, and with dedupe the 49
distinct URLs are parsed once and every reference yields the same
string, so V8's cached hash makes the repeat lookups nearly free. Some
of the `htmlEscapeJsonString` and buffer-layer drops have the same
cause.

### React-level CPU in isolation

The e2e numbers above include everything downstream of React (escaping,
encoding, compression, the SSR client). To see React's own serialization
cost, 114 import rows of dashboard-shaped metadata (49 shared
74-character chunk names per row) were rendered against one request on
the production bundles with a no-op destination, arms interleaved,
median of 5 rounds × 200:

| | main | this PR |
|---|---|---|
| 49 names shared by all rows | 0.502 ms | **0.322 ms** (−36%) |
| 5586 unique names, nothing to dedupe | 0.477 ms | 0.771 ms (+62%) |

The second row is the worst case for this change, a manifest where every
chunk name appears once. It costs about 40 ns per unique string, plus
about 130 ns for each row the budget lets it outline, against a payload
that is otherwise unchanged.

The metadata is serialized by copying it with the strings already
replaced and then calling plain `JSON.stringify`; a `stringify` replacer
function would keep V8 off its fast path for the whole call (measured
2.3× slower than plain in isolation, even writing a sixth of the bytes).
The copy covers plain JSON only and falls back to the replacer for
anything else (`toJSON`, class instances, keys that exist on
`Object.prototype`, depth over four, which is how cycles end up throwing
stringify's own error). Equivalence of the two paths was checked by a
harness that runs both on identical requests and compares the JSON and
the resulting request state: 2,656,142 cases, including exhaustive
enumeration of small trees over adversarial atoms, 100k seeded random
values, and the cases from two independent adversarial reviews — 0
divergences outside four stated assumptions that no bundler manifest
violates (no Proxies, no index accessors polluted onto
`Array.prototype`, no primitive wrappers with a swapped prototype,
side-effect-free property access).

## Cost where there's nothing to dedupe

React's own `flight-ssr-bench` fixture has about ten client modules and
no repeated chunk paths, so the dedupe never fires and the change can
only cost. It costs a little, if anything. Over 16 boots at `aed4d523`
the Flight+Fizz Node sync variant was +0.9% ±0.7 on median inject time
(p=0.008), worse on 14 of 16 boots. On the current head the four
Flight+Fizz inject cells are between +0.4% and +0.8% on the median, none
below p=0.07; across all 88 fixture metrics (Fizz and Flight+Fizz, Node
and Edge, sync and async, inject and HTTP at c=1/c=10) nothing reaches
p<0.01 and `heapMb` is flat to ±0.1%. So the no-dedupe cost is somewhere
around half a percent of inject time on this fixture, at the edge of
what it can resolve.

I couldn't localize it past that. It isn't the per-request `Map`, which
is about 22 ns against a 14 ms render, and it isn't allocation — `gcMs`
and `heapMb` are flat. Using the Fizz-only variants as a within-boot
control, since nothing in `ReactFlightServer.js` can reach them, the
Flight-specific residual on that cell is +0.8% ±0.5 and the other three
variants scatter around zero (+0.4%, +0.1%, −0.2%). A build that
re-inlines `escapeStringValue` back into the string branch, which is the
only change here that runs for every string in the model rather than
only for import metadata, doesn't recover it either (+0.2% ±0.3 on the
same cell, another 16 boots). So this looks like code layout rather than
a specific added operation, and it's near the resolution limit of the
fixture.

<details>
<summary>Verification</summary>

- Both arms' payloads for `/dashboard` were parsed and their
`$`-references resolved recursively, then deep-compared: the resolved
models are identical. The 49 extra model rows are exactly the 49
distinct chunk URLs. All 114 import rows match after resolution.
- Arms fingerprint distinctly (`a898f40a7bbd` vs `87fb4b7ba15e`), so the
two builds are genuinely different.
- Build fingerprints differ between arms (`04440a11435d` vs
`43d09027ce58`) and the arm version strings carry the expected shas.
- Per-boot deltas are printed by the harness; on `/dashboard` serial
req/s all 16 boots are positive (range +11.2% to +22.9%).
- The bench fixture sets a deployment id, so every chunk URL carries a
`?dpl=` query param that exactly doubles its length (74 chars vs 37). An
app without one would see roughly half the absolute byte saving on this
route. The CPU wins that come from string identity rather than byte
count should degrade less than proportionally, but that wasn't measured.
- Not measured: payloads that exceed the 32 KiB tracking budget, and
whether 16 is optimal rather than merely low enough.

</details>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:46:34 +01:00
Hendrik Liebau 77ed3f5452 [Flight/Fizz] Stop the caller's signal from retaining a finished render (#37315)
Every server entry point that accepts a `signal` attached an abort
listener to it and only ever removed that listener from inside the
listener itself. On the success path the signal never aborts, so the
listener stayed attached and its closure kept the whole `Request`, and
therefore the entire rendered output, reachable for as long as the
caller's signal lived.

This matters most for composite signals from `AbortSignal.any()` and for
timeout signals, because the runtime retains those for as long as they
carry a non-weak abort listener, and releases them only when the last
listener is removed or the signal aborts. A composite passed to
`prerender()` therefore became a garbage collection root holding a
finished render for the lifetime of the process. A plain
`AbortController` signal is never retained that way, but it still keeps
the render reachable for as long as the caller holds the controller.

Each listener is now bound to a lifetime signal passed to
`addEventListener`, so the runtime removes the listener as soon as that
signal aborts and nothing has to track a teardown function. Flight
reuses `request.cacheController`, which already aborts on a fatal error,
at the completion of the flush loop (depends on #37342), and in
`abort()`. Fizz has no equivalent, so it gains a
`renderLifetimeController` that aborts at those same three points.
`processReply` creates its controller only when a caller passes a
signal, so a reply without one allocates nothing. Since `abort()`
returns early once the request is past `OPEN`, removing the listener at
those points cannot change observable behavior. The fifty-two copies of
the listener block across the entry points collapse to a single
`attachAbortSignal` call each.

Binding the listener to the render also covers a cancelled stream, which
calls `abort()` without the request ever reaching a terminal status, so
a teardown driven by that status would have left the listener attached.
Fizz ends the lifetime in `fatalError` rather than at the `CLOSING` to
`CLOSED` transition, because a shell error rejects before the caller
receives a stream. Nothing then consumes the request, it never closes,
and a listener waiting for that transition would never come off.

The two new controllers are aborted with an explicit reason. A call to
`abort()` without one constructs an `AbortError` DOMException. Capturing
the stack trace dominates that cost, and the cost grows with the depth
of the stack, so every render and every reply would pay for an object
that no code reads.

`processReply` no longer returns its `abort` function, because that
return value existed only so each `encodeReply` implementation could
wire the signal up itself, and nothing uses it now that the wiring lives
inside. A reply whose model settles synchronously gets no listener,
since aborting it was already a no-op.

The tests assert on the lifetime signal, because the runtime's removal
does not go through `removeEventListener` and is therefore invisible to
a patched signal. `ReactFlightDOMNode-test` asserts the removal itself
with `getEventListeners` from `node:events`, which jsdom has no
equivalent for.

Two cases stay open. A request whose stream is neither consumed nor
cancelled never ends, and a reply with a part that never settles never
settles either, so both keep their listener.
2026-08-22 17:15:56 +02:00
Hendrik Liebau 8366f3389d [Flight] Transfer key validation of lazy nodes when they are unwrapped (#37258) 2026-08-10 16:18:46 +02:00
Andrew Clark 9b5b4d51e5 [Flight] Add 'pending_weak' to Flight thenable protocol (#37154)
Added behind a new experimental flag, `enableFlightWeakThenables`.

Adds a new thenable status to the Flight protocol: `'pending_weak'`.
Unlike a regular pending thenable, a weak thenable does not block the
stream from closing. If it settles while the stream is still open, its
value is emitted like a normal pending thenable. Otherwise its reference
is left unfulfilled and on the client it stays forever pending, without
erroring, even when the connection closes. It's up to the client to
handle the unresolved promise in an appropriate way.

The motivating use case is being able to encode metadata about a Flight
stream into the response itself. For example, a framework might want to
track whether a page varies by search params. It could represent this in
the response as a `Promise<boolean>` that resolves to `true` as soon as
the component being rendered in the stream accesses search params. If
the thenable never resolves by the time the stream closes, then the
client knows that no search params were ever accessed.

In the future we could add a higher-level API for encoding this kind of
information. For now, we intentionally start with the low-level
primitive so frameworks can experiment in userspace without adding
significantly to React's surface area.

Internally, Flight already uses its own private thenable statuses, like
`'resolved_model'`, and the protocol is designed to treat any status
besides `'fulfilled'` and `'rejected'` as equivalent to `'pending'`, so
`'pending_weak'` slots into the existing machinery. On the wire, a weak
reference is encoded as `$w<id>`, next to `$@<id>` for regular promises,
so the client knows its row may intentionally never arrive. On the
client, a weak reference behaves like any other pending promise until
the response closes; then, instead of erroring, it is left forever
pending.
2026-07-31 13:22:30 -04:00
Sebastian "Sebbie" Silbermann 6cb4322d65 [Flight] Port ReplyServer traversal guards to FlightClient (#37144)
Additional defense-in-depth in case consumers pass untrusted input into
Flight Client.

Flight Client generally assumes trusted input.

We'll reserve these kind of fixes for Flight Client in case the
untrusted input leads to catastrophic vulnerabilities e.g. prototype
pollutions that can be used for remote code executions.
2026-07-29 18:17:33 -04:00
Sebastian "Sebbie" SilbermannandClaude Fable 5 9ceb1e7d9e [Flight] Define Flight chunk .then with Object.defineProperty (#37109)
[Secure Ecmascript](https://github.com/tc39/proposal-ses) would freeze
the prototype of intrinsics. Since `ReactPromise` inherits the prototype
from `Promise`, it also copies over the writable definition.

Using `defineProperty` on an inherited property is compatible with SES
though. That's also closer to how classes are specced in JS.


---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:30:19 -04:00
Tim Neutkens 711c445bcc [Flight] Limit fake JSX call site stacks to 10 frames (#37086) 2026-07-22 12:29:45 +02:00
Michael Hart ad78e251e2 [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
Josh StoryandHendrik Liebau d9158919c5 [Flight] Prune debug info when chunks error (#36782)
Flight filters debug information by the consumer end time when a model
initializes successfully. If the stream errors while the model is
pending,
already parsed debug information previously remained unfiltered and
could
produce stacks for work after the cutoff.

Apply the same cutoff when transitioning a chunk to the errored state.
Truncate
the existing debug info array in place because the suspended Lazy
already
references that array, and Fizz reads the Lazy's debug info during
abort.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-06-15 19:36:11 +02:00
Sebastian "Sebbie" Silbermann dbc37501ff Update required references to GitHub repo (#36752) 2026-06-12 09:50:15 +02:00
Sam Zhou 900ae094d8 [flow] Bump flow to v0.317.0 (#36701)
## Summary

Mostly changing the casting syntax from `(x: Y)` to `x as Y`, as the old
syntax was deprecated and always causing an error in Flow

## How did you test this change?

`yarn flow-ci`
2026-06-05 19:17:18 -04:00
Sam Zhou fbb137059e [flow] Bump flow to v0.307.1 (#36199)
## Summary

Notable changes:
- Only $FlowFixMe, $FlowExpectedError suppression comments are supported
- All suppression comments need error code
- A lot of new invalid-compare and constant-condition errors suppressed.
These errors reveal potential logical bugs

## How did you test this change?

`yarn flow-ci`
2026-06-05 13:54:38 -04:00
Hendrik Liebau f0dfee38f8 [Flight] Avoid main-thread stalls from large debug strings (#36570) 2026-05-29 14:27:50 +02:00
Sebastian "Sebbie" SilbermannandHendrik Liebau dd453071d9 [FlightReply] Type hardening and performance improvements (#36425)
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-05-06 19:39:45 +02:00
Xinzi Zhou ad5dfc82b7 Add react-flight-server-fb package for Meta's internal bundler (#36309)
<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->

- Adds a new react-flight-server-fb package providing RSC Flight
bindings for Meta's internal bundler stack
- Unlike webpack/turbopack integrations, this uses no manifest. Module
metadata is self-contained in ClientReference objects and sent over the
wire as-is
- Registers dom-browser-fb and dom-node-fb host configs for Rollup
builds targeting FB_WWW_DEV and FB_WWW_PROD

 Key design differences from other bundler

 - No build-time manifest
- Module IDs use Haste module names (e.g. `"MyComponent"`), with named
exports encoded as `"Module#export"`, rather than file paths resolved
through a manifest
- Client-side loading uses `Bootloader.handlePayload()` +
`JSResource().load()`
- `resolveClientReferenceMetadata` and `resolveClientReference` are
pass-throughs

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

E2E integration test is set up on Meta's internal system.
2026-04-27 09:48:37 -07:00
Sebastian "Sebbie" Silbermann 74568e8627 [Flight] Transport AggregateErrors.errors (#36156) 2026-03-28 18:18:21 -07:00
Hendrik Liebau 5e9eedb578 [Flight] Clear chunk reason after successful module initialization (#36024)
When `requireModule` triggers a reentrant `readChunk` on the same module
chunk, the reentrant call can fail and set `chunk.reason` to an error.
After the outer `requireModule` succeeds, the chunk transitions to
initialized but retains the stale error as `reason`.

When the Flight response stream later closes, it iterates all chunks and
expects `reason` on initialized chunks to be a `FlightStreamController`.
Since the stale `reason` is an `Error` object instead, calling
`chunk.reason.error()` crashes with `TypeError: chunk.reason.error is
not a function`.

The reentrancy can occur when module evaluation synchronously triggers
`readChunk` on the same chunk — for example, when code called during
evaluation tries to resolve the client reference for the module that is
currently being initialized. In Fizz SSR, `captureOwnerStack()` can
trigger this because it constructs component stacks that resolve lazy
client references via `readChunk`. The reentrant `requireModule` call
returns the module's namespace object, but since the module is still
being evaluated, accessing the export binding throws a TDZ (Temporal
Dead Zone) `ReferenceError`. This sets the chunk to the errored state,
and the `ReferenceError` becomes the stale `chunk.reason` after the
outer call succeeds.

This scenario is triggered in Next.js when a client module calls an
instrumented API like `Math.random()` in module scope, which
synchronously invokes `captureOwnerStack()`.
2026-03-12 19:17:24 +01:00
Sebastian "Sebbie" Silbermann 5e4279134d [noop] Typecheck react-noop-renderer against host config and renderer API (#35944) 2026-03-04 13:52:11 +01:00
Sebastian "Sebbie" Silbermann 2ba3065527 [Flight] Add support for transporting Error.cause (#35810) 2026-02-19 15:50:34 -08:00
Tim NeutkensandHendrik Liebau f247ebaf44 [Flight] Walk parsed JSON instead of using reviver for parsing RSC payload (#35776)
## Summary

Follow-up to https://github.com/vercel/next.js/pull/89823 with the
actual changes to React.

Replaces the `JSON.parse` reviver callback in `initializeModelChunk`
with a two-step approach: plain `JSON.parse()` followed by a recursive
`reviveModel()` post-process (same as in Flight Reply Server). This
yields a **~75% speedup** in RSC chunk deserialization.

| Payload | Original (ms) | Walk (ms) | Speedup |
|---------|---------------|-----------|---------|
| Small (2 elements, 142B) | 0.0024 | 0.0007 | **+72%** |
| Medium (~12 elements, 914B) | 0.0116 | 0.0031 | **+73%** |
| Large (~90 elements, 16.7KB) | 0.1836 | 0.0451 | **+75%** |
| XL (~200 elements, 25.7KB) | 0.3742 | 0.0913 | **+76%** |
| Table (1000 rows, 110KB) | 3.0862 | 0.6887 | **+78%** |

## Problem

`createFromJSONCallback` returns a reviver function passed as the second
argument to `JSON.parse()`. This reviver is called for **every key-value
pair** in the parsed JSON. While the logic inside the reviver is
lightweight, the dominant cost is the **C++ → JavaScript boundary
crossing** — V8's `JSON.parse` is implemented in C++, and calling back
into JavaScript for every node incurs significant overhead.

Even a trivial no-op reviver `(k, v) => v` makes `JSON.parse` **~4x
slower** than bare `JSON.parse` without a reviver:

```
108 KB payload:
  Bare JSON.parse:    0.60 ms
  Trivial reviver:    2.95 ms  (+391%)
```

## Change

Replace the reviver with a two-step process:

1. `JSON.parse(resolvedModel)` — parse the entire payload in C++ with no
callbacks
2. `reviveModel` — recursively walk the resulting object in pure
JavaScript to apply RSC transformations

The `reviveModel` function includes additional optimizations over the
original reviver:
- **Short-circuits plain strings**: only calls `parseModelString` when
the string starts with `$`, skipping the vast majority of strings (class
names, text content, etc.)
- **Stays entirely in JavaScript** — no C++ boundary crossings during
the walk

## Results

You can find the related applications in the [Next.js PR
](https://github.com/vercel/next.js/pull/89823)as I've been testing this
on Next.js applications.

### Table as Server Component with 1000 items

Before:

```
    "min": 13.782875000000786,
    "max": 22.23400000000038,
    "avg": 17.116868530000083,
    "p50": 17.10766700000022,
    "p75": 18.50787499999933,
    "p95": 20.426249999998618,
    "p99": 21.814125000000786
```

After:

```
    "min": 10.963916999999128,
    "max": 18.096083000000363,
    "avg": 13.543286884999988,
    "p50": 13.58350000000064,
    "p75": 14.871791999999914,
    "p95": 16.08429099999921,
    "p99": 17.591458000000785
```

### Table as Client Component with 1000 items

Before:

```
    "min": 3.888875000000553,
    "max": 9.044959000000745,
    "avg": 4.651271475000067,
    "p50": 4.555749999999534,
    "p75": 4.966624999999112,
    "p95": 5.47754200000054,
    "p99": 6.109499999998661
````

After:

```
    "min": 3.5986250000005384,
    "max": 5.374291000000085,
    "avg": 4.142990245000046,
    "p50": 4.10570799999914,
    "p75": 4.392041999999492,
    "p95": 4.740084000000934,
    "p99": 5.1652500000000146
```

### Nested Suspense

Before:

```
  Requests:  200
  Min:       73ms
  Max:       106ms
  Avg:       78ms
  P50:       77ms
  P75:       80ms
  P95:       85ms
  P99:       94ms
```

After:

```
  Requests:  200
  Min:       56ms
  Max:       67ms
  Avg:       59ms
  P50:       58ms
  P75:       60ms
  P95:       65ms
  P99:       66ms
```

### Even more nested Suspense (double-level Suspense)

Before:

```
  Requests:  200
  Min:       159ms
  Max:       208ms
  Avg:       169ms
  P50:       167ms
  P75:       173ms
  P95:       183ms
  P99:       188ms
```

After:

```
  Requests:  200
  Min:       125ms
  Max:       170ms
  Avg:       134ms
  P50:       132ms
  P75:       138ms
  P95:       148ms
  P99:       160ms
```

## How did you test this change?

Ran it across many Next.js benchmark applications.

The entire Next.js test suite passes with this change.

---------

Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2026-02-19 08:37:41 -08:00
Hendrik Liebau 47d1ad1454 [Flight] Skip transferReferencedDebugInfo during debug info resolution (#35795)
When the Flight Client resolves chunk references during model parsing,
it calls `transferReferencedDebugInfo` to propagate debug info entries
from referenced chunks to the parent chunk. Debug info on chunks is
later moved to their resolved values, where it is used by React DevTools
to show performance tracks and what a component was suspended by.

Debug chunks themselves (specifically `ReactComponentInfo`,
`ReactAsyncInfo`, `ReactIOInfo`, and their outlined references) are
metadata that is never rendered. They don't need debug info attached to
them. Without this fix, debug info entries accumulate on outlined debug
chunks via their references to other debug chunks (e.g. owner chains and
props deduplication paths). Since each outlined chunk's accumulated
entries are copied to every chunk that references it, this creates
exponential growth in deep component trees, which can cause the dev
server to hang and run out of memory.

This generalizes the existing skip of `transferReferencedDebugInfo` for
Element owner/stack references (which already recognizes that references
to debug chunks don't need debug info transferred) to all references
resolved during debug info resolution. It adds an
`isInitializingDebugInfo` flag set in `initializeDebugChunk` and
`resolveIOInfo`, which propagates through all nested
`initializeModelChunk` calls within the same synchronous stack. For the
async path, `waitForReference` captures the flag at call time into
`InitializationReference.isDebug`, so deferred fulfillments also skip
the transfer.
2026-02-16 09:22:32 -08:00
Sebastian "Sebbie" Silbermann eab523e2a9 [Fiber] Avoid duplicate debug info for array children (#35733) 2026-02-09 20:36:56 +01:00
Hendrik Liebau 272441a9ad [Flight] Add unstable_allowPartialStream option to Flight Client (#35731)
When using a partial prerender stream, i.e. a prerender that is
intentionally aborted before all I/O has resolved, consumers of
`createFromReadableStream` would need to keep the stream unclosed to
prevent React Flight from erroring on unresolved chunks. However, some
browsers (e.g. Chrome, Firefox) keep unclosed ReadableStreams with
pending reads as native GC roots, retaining the entire Flight response.

With this PR we're adding an `unstable_allowPartialStream` option, that
allows consumers to close the stream normally. The Flight Client's
`close()` function then transitions pending chunks to halted instead of
erroring them. Halted chunks keep Suspense fallbacks showing (i.e. they
never resolve), and their `.then()` is a no-op so no new listeners
accumulate. Inner stream chunks (ReadableStream/AsyncIterable) are
closed gracefully, and `getChunk()` returns halted chunks for new IDs
that are accessed after closing the response. Blocked chunks are left
alone because they may be waiting on client-side async operations like
module loading, or on forward references to chunks that appeared later
in the stream, both of which resolve independently of closing.
2026-02-09 19:19:32 +01:00
Hendrik Liebau b07aa7d643 [Flight] Fix encodeReply for JSX with temporary references (#35730)
`encodeReply` throws "React Element cannot be passed to Server Functions
from the Client without a temporary reference set" when a React element
is the root value of a `serializeModel` call (either passed directly or
resolved from a promise), even when a temporary reference set is
provided.

The cause is that `resolveToJSON` hits the `REACT_ELEMENT_TYPE` switch
case before reaching the `existingReference`/`modelRoot` check that
regular objects benefit from. The synthetic JSON root created by
`JSON.stringify` is never tracked in `writtenObjects`, so
`parentReference` is `undefined` and the code falls through to the
throw. This adds a `modelRoot` check in the `REACT_ELEMENT_TYPE` case,
following the same pattern used for promises and plain objects.

The added `JSX as root model` test also uncovered a pre-existing crash
in the Flight Client: when the JSX element round-trips back, it arrives
as a frozen object (client-created elements are frozen in DEV), and
`Object.defineProperty` for `_debugInfo` fails because frozen objects
are non-configurable. The same crash can occur with JSX exported as a
client reference. For now, we're adding `!Object.isFrozen()` guards in
`moveDebugInfoFromChunkToInnerValue` and `addAsyncInfo` to prevent the
crash, which means debug info is silently dropped for frozen elements.
The proper fix would likely be to clone the element so each rendering
context gets its own mutable copy with correct debug info.

closes #34984
closes #35690
2026-02-09 16:17:53 +01:00
Sebastian "Sebbie" Silbermann ed4bd540ca [Flight] Warn once if eval is disabled in dev environment (#35661) 2026-02-02 12:56:14 +01:00
Sebastian "Sebbie" Silbermann c0c37063e2 [Flight] Restore original function name in dev, server callstacks served with unsafe-eval (#35650) 2026-01-28 18:41:08 +01:00
Ricky e66ef6480e [tests] remove withoutStack from assertConsole helpers (#35498)
Stacked on https://github.com/facebook/react/pull/35497

-----

Now that the assert helpers require a component stack, we don't need the
`{withoutStack: true}` option.
2026-01-27 22:34:03 -05:00
Sebastian "Sebbie" Silbermann 8c34556ca8 [Flight] Fix react-markup types for server references (#35634) 2026-01-26 21:13:16 +01:00
10680271fa [Flight] Add more DoS mitigations to Flight Reply, and harden Flight (#35632)
This fixes security vulnerabilities in Server Functions.

---------

Co-authored-by: Sebastian Markbåge <sebastian@calyptus.eu>
Co-authored-by: Josh Story <josh.c.story@gmail.com>
Co-authored-by: Janka Uryga <lolzatu2@gmail.com>
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>
2026-01-26 20:24:58 +01:00
Ruslan Lesiutin 94913cbffe [flags] cleanup renameElementSymbol (#35600)
Removed the feature flag completely, enabled by default. Will land once
I have everything ready on xplat side.
2026-01-23 10:46:30 +00:00
Ricky 3e1abcc8d7 [tests] Require exact error messages in assertConsole helpers (#35497)
Requires full error message in assert helpers. 

Some of the error messages we asset on add a native javascript stack
trace, which would be a pain to add to the messages and maintain. This
PR allows you to just add `\n in <stack>` placeholder to the error
message to denote a native stack trace is present in the message.

---
Note: i vibe coded this so it was a pain to backtrack this to break this
into a stack, I tried and gave up, sorry.
2026-01-13 15:52:53 -05:00
Hendrik Liebau 454fc41fc7 [test] Add tests for cyclic arrays in Flight and Flight Reply (#35347)
We already had tests for cyclic objects, but not for cyclic arrays.
2025-12-17 18:08:16 +01:00
Sebastian "Sebbie" Silbermann ba5b843692 [test] Exclude repository root from assertions (#35361) 2025-12-15 11:45:17 +01:00
Sebastian Markbåge 894bc73cb4 [Flight] Patch Promise cycles and toString on Server Functions (#35345)
Server Functions can be stringified (sometimes implicitly) when passed
as data. This adds an override to hide the source code in that case -
just in case someone puts sensitive information in there.

Note that this still preserves the `name` field but this is also
available on the export but in practice is likely minified anyway.
There's nothing else on these referenes we'd consider unsafe unless you
explicitly expose expandos which are part of the `"use server"` export.

This adds a safety check to ensure you don't encode cyclic Promises.
This isn't a parser bug per se. Promises do have a safety mechanism that
avoids them infinite looping. However, since we use custom Thenables,
what can happen is that every time a native Promise awaits it, another
Promise wrapper is created around the Thenable which foils the
ECMAScript Promise cycle detection which can lead to an infinite loop.

This also ensures that embedded `ReadableStream` and `AsyncIterable`
streams are properly closed if the source stream closes early both on
the Server and Client. This doesn't cause an infinite loop but just to
make sure resource clean up can proceed properly.

We're also adding some more explicit clear errors for invalid payloads
since we no longer need to obfuscate the original issue.
2025-12-11 15:24:24 -05:00
Sebastian "Sebbie" SilbermannandHendrik Liebau 378973b387 [Flight] Move react-server-dom-webpack/*.unbundled to private react-server-dom-unbundled (#35290)
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
2025-12-05 03:59:21 +01:00
Sebastian Markbåge eb89912ee5 Add expertimental optimisticKey behind a flag (#35162)
When dealing with optimistic state, a common problem is not knowing the
id of the thing we're waiting on. Items in lists need keys (and single
items should often have keys too to reset their state). As a result you
have to generate fake keys. It's a pain to manage those and when the
real item comes in, you often end up rendering that with a different
`key` which resets the state of the component tree. That in turns works
against the grain of React and a lot of negatives fall out of it.

This adds a special `optimisticKey` symbol that can be used in place of
a `string` key.

```js
import {optimisticKey} from 'react';
...
const [optimisticItems, setOptimisticItems] = useOptimistic([]);
const children = savedItems.concat(
  optimisticItems.map(item =>
    <Item key={optimisticKey} item={item} />
  )
);
return <div>{children}</div>;
```

The semantics of this `optimisticKey` is that the assumption is that the
newly saved item will be rendered in the same slot as the previous
optimistic items. State is transferred into whatever real key ends up in
the same slot.

This might lead to some incorrect transferring of state in some cases
where things don't end up lining up - but it's worth it for simplicity
in many cases since dealing with true matching of optimistic state is
often very complex for something that only lasts a blink of an eye.

If a new item matches a `key` elsewhere in the set, then that's favored
over reconciling against the old slot.

One quirk with the current algorithm is if the `savedItems` has items
removed, then the slots won't line up by index anymore and will be
skewed. We might be able to add something where the optimistic set is
always reconciled against the end. However, it's probably better to just
assume that the set will line up perfectly and otherwise it's just best
effort that can lead to weird artifacts.

An `optimisticKey` will match itself for updates to the same slot, but
it will not match any existing slot that is not an `optimisticKey`. So
it's not an `any`, which I originally called it, because it doesn't
match existing real keys against new optimistic keys. Only one
direction.
2025-11-18 16:29:18 -05:00
Hendrik LiebauandSebastian Sebbie Silbermann fb2177c153 [Flight] Fix pending chunks count for streams & async iterables in DEV (#35143)
In DEV, we need to prevent the response from being GC'd while there are
still pending chunks for ReadableSteams or pending results for
AsyncIterables.

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-11-14 23:52:11 +01:00
Hendrik Liebau 93fc57400b [Flight] Fix broken byte stream parsing caused by buffer detachment (#35127)
This PR fixes a critical bug where `ReadableStream({type: 'bytes'})`
instances passed through React Server Components (RSC) would stall after
reading only the first chunk or the first few chunks in the client. This
issue was masked by using `web-streams-polyfill` in tests, but manifests
with native Web Streams implementations.

The root cause is that when a chunk is enqueued to a
`ReadableByteStreamController`, the spec requires the underlying
ArrayBuffer to be synchronously transferred/detached. In the React
Flight Client's chunk parsing, embedded byte stream chunks are created
as views into the incoming RSC stream chunk buffer using `new
Uint8Array(chunk.buffer, offset, length)`. When embedded byte stream
chunks are enqueued, they can detach the shared buffer, leaving the RSC
stream parsing in a broken state.

The fix is to copy embedded byte stream chunks before enqueueing them,
preventing buffer detachment from affecting subsequent parsing. To not
affect performance too much, we use a zero-copy optimization: when a
chunk ends exactly at the end of the RSC stream chunk, or when the row
spans into the next RSC chunk, no further parsing will access that
buffer, so we can safely enqueue the view directly without copying.

We now also enqueue embedded byte stream chunks immediately as they are
parsed, without waiting for the full row to complete.

To simplify the logic in the client, we introduce a new `'b'` protocol
tag specifically for byte stream chunks. The server now emits `'b'`
instead of `'o'` for `Uint8Array` chunks from byte streams (detected via
`supportsBYOB`). This allows the client to recognize byte stream chunks
without needing to track stream IDs.

Tests now use the proper Jest environment with native Web Streams
instead of polyfills, exposing and validating the fix for this issue.
2025-11-13 21:23:02 +01:00
Sebastian Markbåge dd048c3b2d Clean up enablePostpone Experiment (#35048)
We're not shipping this and it's a lot of code to maintain that is
blocking my refactor of Fizz for SuspenseList.
2025-11-05 00:05:59 -05:00
Hendrik Liebau 67f7d47a9b [Flight] Fix debug info filtering to include later resolved I/O (#35036)
In #35019, we excluded debug I/O info from being considered for
enhancing the owner stack if it resolved after the defined `endTime`
option that can be passed to the Flight client. However, we should
include any I/O that was awaited before that end time, even if it
resolved later.
2025-11-03 22:59:40 +01:00
Hendrik Liebau 561ee24d4a [Fizz] Push halted await to the owner stack for late-arriving I/O info (#35019) 2025-11-01 16:03:09 +01:00
Hendrik Liebau 0d721b60c2 [Flight] Don't hang after resolving cyclic references (#34988) 2025-10-27 22:06:28 +01:00
Sebastian Markbåge 21272a680f Lower case "rsc stream" debug info (#34921)
This is an aesthetic thing. Most simple I/O entries are things like
"script", "stylesheet", "fetch" etc. which are all a single word and
lower case. The "RSC stream" name sticks out and draws unnecessary
attention to itself where as it's really the least interesting to look
at.

I don't love the name because I'm not sure how to explain it. It's
really mainly the byte size of the payload itself without considering
things like server awaits things which will have their own cause. So I'm
trying to communicate the download size of the stream of downloading the
`.rsc` file or the `"rsc stream"`.
2025-10-20 02:42:38 -04:00
Sebastian Markbåge 2cfb221937 [Flight] Allow passing DEV only startTime as an option (#34912)
When you use the `createFromFetch` API we assume that the start time of
the request is the same time as when you call `createFromFetch` but in
principle you could use it with a Promise that starts earlier and just
happens to resolve to a `Response`.

When you use `createFromReadableStream` that is almost definitely the
case. E.g. you might have started it way earlier and you don't call
`createFromReadableStream` until you get the headers back (the fetch
promise resolves).

This adds an option to pass in the start time for debug purposes if you
started the request before starting to parse it.
2025-10-19 16:38:33 -04:00
Sebastian Markbåge 56e846921d [Flight] Exclude RSC Stream if the stream resolves in a task (#34838) 2025-10-14 14:28:47 +02:00