Skip to content

[Flight] Skip transferReferencedDebugInfo during debug info resolution - #35795

Merged
unstubbable merged 2 commits into
react:mainfrom
unstubbable:debug-chunks-oom
Feb 16, 2026
Merged

unstubbable merged 2 commits into
react:mainfrom
unstubbable:debug-chunks-oom

Conversation

@unstubbable

Copy link
Copy Markdown
Collaborator

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.

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.
@meta-cla meta-cla Bot added the CLA Signed label Feb 16, 2026
@github-actions github-actions Bot added the React Core Team Opened by a member of the React Core Team label Feb 16, 2026
@react-sizebot

react-sizebot commented Feb 16, 2026

Copy link
Copy Markdown

Comparing: e8c6362...d6cd9a3

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 611.02 kB 611.02 kB = 108.00 kB 108.00 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 676.95 kB 676.95 kB = 118.97 kB 118.97 kB
facebook-www/ReactDOM-prod.classic.js = 697.68 kB 697.68 kB = 122.67 kB 122.67 kB
facebook-www/ReactDOM-prod.modern.js = 687.99 kB 687.99 kB = 121.05 kB 121.05 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/react-client/cjs/react-client-flight.development.js +0.30% 180.58 kB 181.12 kB +0.25% 31.35 kB 31.43 kB
oss-stable/react-client/cjs/react-client-flight.development.js +0.30% 180.61 kB 181.15 kB +0.25% 31.38 kB 31.46 kB
oss-experimental/react-client/cjs/react-client-flight.development.js +0.30% 180.61 kB 181.15 kB +0.25% 31.38 kB 31.46 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-client.browser.development.js +0.29% 186.32 kB 186.87 kB +0.25% 32.68 kB 32.77 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-client.browser.development.js +0.29% 186.37 kB 186.92 kB +0.25% 32.71 kB 32.79 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.browser.development.js +0.29% 186.39 kB 186.93 kB +0.25% 32.71 kB 32.79 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js +0.29% 186.93 kB 187.48 kB +0.26% 33.07 kB 33.16 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js +0.29% 186.93 kB 187.48 kB +0.26% 33.07 kB 33.16 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-client.edge.development.js +0.29% 186.93 kB 187.48 kB +0.26% 33.07 kB 33.16 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.29% 188.36 kB 188.90 kB +0.27% 33.10 kB 33.19 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.29% 188.41 kB 188.95 kB +0.27% 33.12 kB 33.21 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.browser.development.js +0.29% 188.42 kB 188.96 kB +0.27% 33.13 kB 33.21 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js +0.29% 190.28 kB 190.82 kB +0.25% 33.60 kB 33.68 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js +0.29% 190.28 kB 190.82 kB +0.25% 33.60 kB 33.68 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.edge.development.js +0.29% 190.28 kB 190.82 kB +0.25% 33.60 kB 33.68 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js +0.29% 190.30 kB 190.85 kB +0.25% 33.62 kB 33.70 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js +0.29% 190.30 kB 190.85 kB +0.25% 33.62 kB 33.70 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.edge.development.js +0.29% 190.30 kB 190.85 kB +0.25% 33.62 kB 33.70 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.28% 190.92 kB 191.47 kB +0.27% 33.57 kB 33.66 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.28% 190.97 kB 191.52 kB +0.26% 33.59 kB 33.68 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.browser.development.js +0.28% 190.98 kB 191.53 kB +0.26% 33.60 kB 33.68 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.28% 191.54 kB 192.09 kB +0.27% 33.74 kB 33.83 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.28% 191.59 kB 192.14 kB +0.27% 33.76 kB 33.85 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js +0.28% 191.61 kB 192.15 kB +0.27% 33.77 kB 33.86 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.28% 191.72 kB 192.27 kB +0.25% 33.78 kB 33.86 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.28% 191.72 kB 192.27 kB +0.25% 33.78 kB 33.86 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-client.node.development.js +0.28% 191.72 kB 192.27 kB +0.25% 33.78 kB 33.86 kB
oss-experimental/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js +0.28% 193.73 kB 194.27 kB +0.24% 33.89 kB 33.98 kB
oss-stable-semver/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js +0.28% 193.73 kB 194.27 kB +0.24% 33.89 kB 33.98 kB
oss-stable/react-server-dom-parcel/cjs/react-server-dom-parcel-client.node.development.js +0.28% 193.73 kB 194.27 kB +0.24% 33.89 kB 33.98 kB
oss-experimental/react-server-dom-unbundled/cjs/react-server-dom-unbundled-client.node.development.js +0.28% 195.38 kB 195.92 kB +0.25% 34.14 kB 34.23 kB
oss-stable-semver/react-server-dom-unbundled/cjs/react-server-dom-unbundled-client.node.development.js +0.28% 195.38 kB 195.92 kB +0.25% 34.14 kB 34.23 kB
oss-stable/react-server-dom-unbundled/cjs/react-server-dom-unbundled-client.node.development.js +0.28% 195.38 kB 195.92 kB +0.25% 34.14 kB 34.23 kB
oss-experimental/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js +0.28% 196.78 kB 197.32 kB +0.24% 34.41 kB 34.49 kB
oss-stable-semver/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js +0.28% 196.78 kB 197.32 kB +0.24% 34.41 kB 34.49 kB
oss-stable/react-server-dom-turbopack/cjs/react-server-dom-turbopack-client.node.development.js +0.28% 196.78 kB 197.32 kB +0.24% 34.41 kB 34.49 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js +0.28% 196.80 kB 197.34 kB +0.25% 34.43 kB 34.51 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js +0.28% 196.80 kB 197.34 kB +0.25% 34.43 kB 34.51 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-client.node.development.js +0.28% 196.80 kB 197.34 kB +0.25% 34.43 kB 34.51 kB
oss-stable-semver/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.25% 235.88 kB 236.46 kB +0.20% 52.44 kB 52.55 kB
oss-stable/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.25% 235.91 kB 236.49 kB +0.20% 52.46 kB 52.57 kB
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.25% 235.91 kB 236.49 kB +0.20% 52.47 kB 52.57 kB

Generated by 🚫 dangerJS against d6cd9a3

@unstubbable
unstubbable marked this pull request as ready for review February 16, 2026 01:09
Comment thread packages/react-client/src/ReactFlightClient.js Outdated
};
let initializingHandler: null | InitializationHandler = null;
let initializingChunk: null | BlockedChunk<any> = null;
let isInitializingDebugInfo: boolean = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be stashed on the initialization handler? We’d never assign the property so it shouldn’t create more hidden classes in practice

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work, because initializeModelChunk resets initializingHandler = null, so the flag would be lost in nested calls.

@unstubbable
unstubbable merged commit 47d1ad1 into react:main Feb 16, 2026
235 checks passed
@unstubbable
unstubbable deleted the debug-chunks-oom branch February 16, 2026 17:22
unstubbable added a commit to unstubbable/react that referenced this pull request Sep 1, 2026
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 react#37358
proposes for the element props branch and the default branch of
`fulfillReference`. react#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 react#37343, which comes from references in model chunks. react#37343 also
reports the call in `getOutlinedModel` as unguarded, which it is not,
because react#35795 already skips it there.

The rest of react#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. react#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 react#37343
Closes react#37358
Closes react#37359

Co-authored-by: sundeep8967 <71071718+sundeep8967@users.noreply.github.com>
unstubbable added a commit that referenced this pull request Sep 2, 2026
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>
github-actions Bot pushed a commit that referenced this pull request Sep 2, 2026
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>

DiffTrain build for [8f00437](8f00437)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants