Fix memory leak in createPrefetchResponseStream by releasing reader lock#89485
Closed
timneutkens wants to merge 2 commits intocanaryfrom
Closed
Conversation
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Contributor
Tests Passed |
unstubbable
reviewed
Feb 4, 2026
Contributor
Stats from current PR✅ No significant changes detected📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles: **437 kB** → **437 kB**
|
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 766 B | 763 B | ✓ |
| Total | 766 B | 763 B | ✅ -3 B |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 449 B | 451 B | ✓ |
| Total | 449 B | 451 B |
📦 Webpack
Client
Main Bundles
| Canary | PR | Change | |
|---|---|---|---|
| 5528-HASH.js gzip | 5.47 kB | N/A | - |
| 6280-HASH.js gzip | 56.8 kB | N/A | - |
| 6335.HASH.js gzip | 169 B | N/A | - |
| 912-HASH.js gzip | 4.53 kB | N/A | - |
| e8aec2e4-HASH.js gzip | 62.5 kB | N/A | - |
| framework-HASH.js gzip | 59.7 kB | 59.7 kB | ✓ |
| main-app-HASH.js gzip | 256 B | 254 B | ✓ |
| main-HASH.js gzip | 39 kB | 39 kB | ✓ |
| webpack-HASH.js gzip | 1.68 kB | 1.68 kB | ✓ |
| 262-HASH.js gzip | N/A | 4.52 kB | - |
| 2889.HASH.js gzip | N/A | 169 B | - |
| 5602-HASH.js gzip | N/A | 5.48 kB | - |
| 6948ada0-HASH.js gzip | N/A | 62.5 kB | - |
| 9544-HASH.js gzip | N/A | 57.5 kB | - |
| Total | 230 kB | 231 kB |
Polyfills
| Canary | PR | Change | |
|---|---|---|---|
| polyfills-HASH.js gzip | 39.4 kB | 39.4 kB | ✓ |
| Total | 39.4 kB | 39.4 kB | ✓ |
Pages
| Canary | PR | Change | |
|---|---|---|---|
| _app-HASH.js gzip | 194 B | 194 B | ✓ |
| _error-HASH.js gzip | 183 B | 180 B | 🟢 3 B (-2%) |
| css-HASH.js gzip | 331 B | 330 B | ✓ |
| dynamic-HASH.js gzip | 1.81 kB | 1.81 kB | ✓ |
| edge-ssr-HASH.js gzip | 256 B | 256 B | ✓ |
| head-HASH.js gzip | 351 B | 352 B | ✓ |
| hooks-HASH.js gzip | 384 B | 383 B | ✓ |
| image-HASH.js gzip | 580 B | 581 B | ✓ |
| index-HASH.js gzip | 260 B | 260 B | ✓ |
| link-HASH.js gzip | 2.49 kB | 2.49 kB | ✓ |
| routerDirect..HASH.js gzip | 320 B | 319 B | ✓ |
| script-HASH.js gzip | 386 B | 386 B | ✓ |
| withRouter-HASH.js gzip | 315 B | 315 B | ✓ |
| 1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
| Total | 7.97 kB | 7.97 kB | ✅ -1 B |
Server
Edge SSR
| Canary | PR | Change | |
|---|---|---|---|
| edge-ssr.js gzip | 126 kB | 126 kB | ✓ |
| page.js gzip | 249 kB | 249 kB | ✓ |
| Total | 375 kB | 375 kB |
Middleware
| Canary | PR | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 615 B | 616 B | ✓ |
| middleware-r..fest.js gzip | 156 B | 155 B | ✓ |
| middleware.js gzip | 33.1 kB | 33.2 kB | ✓ |
| edge-runtime..pack.js gzip | 842 B | 842 B | ✓ |
| Total | 34.7 kB | 34.8 kB |
Build Details
Build Manifests
| Canary | PR | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 732 B | 736 B | ✓ |
| Total | 732 B | 736 B |
Build Cache
| Canary | PR | Change | |
|---|---|---|---|
| 0.pack gzip | 3.82 MB | 3.82 MB | 🔴 +7.28 kB (+0%) |
| index.pack gzip | 103 kB | 103 kB | ✓ |
| index.pack.old gzip | 103 kB | 103 kB | ✓ |
| Total | 4.02 MB | 4.03 MB |
🔄 Shared (bundler-independent)
Runtimes
| Canary | PR | Change | |
|---|---|---|---|
| app-page-exp...dev.js gzip | 312 kB | 312 kB | ✓ |
| app-page-exp..prod.js gzip | 166 kB | 166 kB | ✓ |
| app-page-tur...dev.js gzip | 312 kB | 312 kB | ✓ |
| app-page-tur..prod.js gzip | 166 kB | 166 kB | ✓ |
| app-page-tur...dev.js gzip | 308 kB | 308 kB | ✓ |
| app-page-tur..prod.js gzip | 164 kB | 164 kB | ✓ |
| app-page.run...dev.js gzip | 309 kB | 309 kB | ✓ |
| app-page.run..prod.js gzip | 164 kB | 164 kB | ✓ |
| app-route-ex...dev.js gzip | 70.4 kB | 70.4 kB | ✓ |
| app-route-ex..prod.js gzip | 48.9 kB | 48.9 kB | ✓ |
| app-route-tu...dev.js gzip | 70.5 kB | 70.5 kB | ✓ |
| app-route-tu..prod.js gzip | 49 kB | 49 kB | ✓ |
| app-route-tu...dev.js gzip | 70.1 kB | 70.1 kB | ✓ |
| app-route-tu..prod.js gzip | 48.7 kB | 48.7 kB | ✓ |
| app-route.ru...dev.js gzip | 70 kB | 70 kB | ✓ |
| app-route.ru..prod.js gzip | 48.7 kB | 48.7 kB | ✓ |
| dist_client_...dev.js gzip | 324 B | 324 B | ✓ |
| dist_client_...dev.js gzip | 326 B | 326 B | ✓ |
| dist_client_...dev.js gzip | 318 B | 318 B | ✓ |
| dist_client_...dev.js gzip | 317 B | 317 B | ✓ |
| pages-api-tu...dev.js gzip | 43.2 kB | 43.2 kB | ✓ |
| pages-api-tu..prod.js gzip | 32.9 kB | 32.9 kB | ✓ |
| pages-api.ru...dev.js gzip | 43.1 kB | 43.1 kB | ✓ |
| pages-api.ru..prod.js gzip | 32.8 kB | 32.8 kB | ✓ |
| pages-turbo....dev.js gzip | 52.5 kB | 52.5 kB | ✓ |
| pages-turbo...prod.js gzip | 39.4 kB | 39.4 kB | ✓ |
| pages.runtim...dev.js gzip | 52.4 kB | 52.4 kB | ✓ |
| pages.runtim..prod.js gzip | 39.3 kB | 39.3 kB | ✓ |
| server.runti..prod.js gzip | 62.6 kB | 62.6 kB | ✓ |
| Total | 2.78 MB | 2.78 MB |
📝 Changed Files (8 files)
Files with changes:
app-page-exp..ntime.dev.jsapp-page-exp..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page-tur..ntime.dev.jsapp-page-tur..time.prod.jsapp-page.runtime.dev.jsapp-page.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
Diff too large to display
app-page-exp..time.prod.js
failed to diffapp-page-tur..ntime.dev.js
Diff too large to display
app-page-tur..time.prod.js
Diff too large to display
app-page-tur..ntime.dev.js
Diff too large to display
app-page-tur..time.prod.js
Diff too large to display
app-page.runtime.dev.js
Diff too large to display
app-page.runtime.prod.js
Diff too large to display
unstubbable
added a commit
that referenced
this pull request
Feb 9, 2026
Previously, `createPrefetchResponseStream` intentionally never called `controller.close()` on the wrapper stream, to prevent React Flight from erroring on unresolved references (dynamic holes). However, Chrome and Firefox keep unclosed ReadableStreams with pending reads as native GC roots, preventing the stream — and the entire FlightResponse captured in the `reader.read().then(progress)` closure chain — from being garbage-collected. Now that React Flight supports `unstable_allowPartialStream` as an option for `createFromReadableStream` (facebook/react#35731), we can close the stream normally. Flight will mark unresolved chunks as "halted" instead of erroring, which is the correct behavior for prefetch responses. Also removes the now-unnecessary `createUnclosingPrefetchStream` wrapper from the legacy prefetch path in `fetch-server-response.ts`. closes #89485
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What?
Fix memory leak in
createPrefetchResponseStreamwhere theReadableStreamDefaultReaderis never released after consuming the original stream.Why?
The unreleased reader maintains a strong reference chain:
originalFlightStream.[[reader]] → reader → closures, preventing garbage collection and causing ~32MB memory growth per 10 navigation cycles on the test app.How?
Add
reader.releaseLock()when the stream completes. This breaks the reference chain while preserving the intentional behavior of keeping the target stream open (for Flight's hanging promise handling in PPR).Test Results