Skip to content

Conversation

unstubbable
Copy link
Collaborator

@unstubbable unstubbable commented Oct 14, 2025

Using renderToReadableStream in Node.js with binary data from fs.readFileSync (or Buffer.allocUnsafe) could cause downstream consumers (like compression middleware) to fail with "Cannot perform Construct on a detached ArrayBuffer".

The issue occurs because Node.js uses an 8192-byte Buffer pool for small allocations (< 4KB). When React's VIEW_SIZE was 2KB, files between ~2KB and 4KB would be passed through as views of pooled buffers rather than copied into currentView. ByteStreams (type: 'bytes') detach ArrayBuffers during transfer, which corrupts the shared Buffer pool and causes subsequent Buffer operations to fail.

Increasing VIEW_SIZE from 2KB to 4KB ensures all chunks smaller than 4KB are copied into currentView (which uses a dedicated 4KB buffer outside the pool), while chunks 4KB or larger don't use the pool anyway. Thus no pooled buffers are ever exposed to ByteStream detachment.

This adds 2KB memory per active stream, copies chunks in the 2-4KB range instead of passing them as views (small CPU cost), and buffers up to 2KB more data before flushing. However, it avoids duplicating large binary data (which copying everything would require, like the Edge entry point currently does in typedArrayToBinaryChunk).

Related issues:

@meta-cla meta-cla bot added the CLA Signed label Oct 14, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Oct 14, 2025
@react-sizebot
Copy link

react-sizebot commented Oct 14, 2025

Comparing: 1324e1b...9aa952a

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.68 kB 6.68 kB = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 605.41 kB 605.41 kB = 107.21 kB 107.22 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 664.38 kB 664.38 kB = 117.09 kB 117.09 kB
facebook-www/ReactDOM-prod.classic.js = 688.25 kB 688.25 kB = 121.13 kB 121.13 kB
facebook-www/ReactDOM-prod.modern.js = 678.67 kB 678.67 kB = 119.48 kB 119.49 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 9aa952a

@unstubbable unstubbable marked this pull request as ready for review October 14, 2025 23:22
Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

Let's discuss alternatives. We explicitly try to avoid cloning large arrays.

It's a little unfortunate in the case of just passing a raw value that isn't part of for example a Response, Blob or a Stream.

But for example, one shot iterables are also consumed in a similar way for performance. If you pass a one shot iterable or ReadableStream it also gets consumed by the algorithm.

Maybe we should clone at least debug values as typed arrays since they're not actually part of it but things passed as props to a client component for example should be able to be consumed.

Regardless the fix is likely to clone at specific types of values in specific contexts (e.g. when serializing a TypedArray but not the content of a Blob) and not at the level of the stream.

Using `renderToReadableStream` in Node.js with binary data from
`fs.readFileSync` or `Buffer.allocUnsafe` could cause downstream
consumers (like compression middleware) to fail with "Cannot perform
Construct on a detached ArrayBuffer".

The issue occurs because Node.js uses an 8192-byte Buffer pool for small
allocations (< 4KB). When React's `VIEW_SIZE` was 2KB, files between
~2KB and 4KB would be passed through as views of pooled buffers rather
than copied into `currentView`. ByteStreams (`type: 'bytes'`) detach
ArrayBuffers during transfer, which corrupts the shared Buffer pool and
causes subsequent Buffer operations to fail.

Increasing `VIEW_SIZE` from 2KB to 4KB ensures all chunks smaller than
4KB are copied into `currentView` (which uses a dedicated 4KB buffer
outside the pool), while chunks 4KB or larger don't use the pool anyway.
No pooled buffers are ever exposed to ByteStream detachment.

This adds 2KB memory per active stream, copies chunks in the 2-4KB range
instead of passing them as views (small CPU cost), and buffers up to 2KB
more data before flushing. However, it avoids duplicating large binary
data (which copying everything would require, like the Edge entry point
currently does in `typedArrayToBinaryChunk`).
@unstubbable unstubbable force-pushed the fix-detached-array-buffer-error branch from 72e2b4b to 3d1193a Compare October 17, 2025 12:02
Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

We should ideally also update the edge one to not clone and see what happens there.

@unstubbable unstubbable merged commit dc485c7 into facebook:main Oct 17, 2025
240 checks passed
@unstubbable unstubbable deleted the fix-detached-array-buffer-error branch October 17, 2025 20:13
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