Dispose dev child entries after their parents are disposed - #96804
Draft
marcoshernanz wants to merge 1 commit into
Draft
Dispose dev child entries after their parents are disposed#96804marcoshernanz wants to merge 1 commit into
marcoshernanz wants to merge 1 commit into
Conversation
Contributor
Tests PassedCommit: 4df5d7c |
Contributor
Stats from current PR🟢 2 improvements
📊 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
Server Middleware
Build DetailsBuild Manifests
Build Cache
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📎 Tarball URLCommit: 4df5d7c |
marcoshernanz
force-pushed
the
marcos/memleak-child-entry
branch
from
August 6, 2026 08:33
a35623d to
c4d2f10
Compare
marcoshernanz
force-pushed
the
marcos/memleak-child-entry
branch
2 times, most recently
from
August 6, 2026 18:11
4df5d7c to
dca9aaa
Compare
The webpack dev server never disposed CHILD_ENTRY records in on-demand-entry-handler (a TODO-APP): disposeInactiveEntries skipped them entirely, so every visited app page left its client-side flight entry in the entries map and in the client webpack compilation for the lifetime of the dev server, accumulating compilation memory during long dev sessions (#54708). A child entry is injected by its parent entries (e.g. the client flight entry of an app page) and is needed exactly as long as at least one of them. This changes the disposal lifecycle in three places: - disposeInactiveEntries recomputes each child entry's disposal state once per pass from the bundle paths of the active ENTRY records: scheduled once no active parent remains, and cleared again if a parent is revived. Recomputing instead of permanently pruning the parent set matters because the disposal flag is only pending until the disposal machinery runs. - hot-reloader-webpack's entry computation, which deletes entries whose disposal flag is set, now consults live parent state for child entries: a child whose parent was revived after the last disposal pass is kept even while it still carries a stale disposal flag. - hot-reloader-rspack rehydrates child parentEntries back into Sets when reading built-entries.json: JSON.stringify serializes Sets as plain objects, which would otherwise crash disposal iteration on restart. Only ENTRY records count as parents: a child entry shares its parent bundle path, so considering every record would keep the parent set alive through the child itself. The pre-existing open PR #89587 attempted this fix but compared parent names against the bundle paths of all undisposed records including the child itself, which always matches and therefore never disposes anything. Adds unit tests covering: stale-parent disposal across passes, removal of gone parents, empty parent sets, shared children with mixed parent states, revival of a parent scheduled for disposal, active-parent computation, and middleware/instrumentation exemptions. Verified end to end in a webpack dev server instrumented to dump the entries map per pass: after maxInactiveAge the parent entries of three visited app pages were scheduled, their child entries were scheduled in the following passes with the parent links kept intact, and the next compilation removed all of them from the map; re-visiting a disposed page revived its entries and rendered fine. Fixes #54708 Co-Authored-By: Marcos Hernanz <96699542+marcoshernanz@users.noreply.github.com>
marcoshernanz
force-pushed
the
marcos/memleak-child-entry
branch
from
August 6, 2026 18:21
dca9aaa to
166c4f6
Compare
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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?
disposeInactiveEntriesnow disposesCHILD_ENTRYrecords once none of their parent entries are active anymore, instead of skipping them entirely (aTODO-APP), and the disposal machinery treats revival and cached restarts correctly.Why?
The webpack dev server never disposed child entries: every visited app page left its client-side flight entry in the entries map and in the client webpack compilation for the lifetime of the dev server, accumulating compilation memory during long dev sessions (#54708).
A child entry is injected by its parent entries (e.g. the client flight entry of an app page) and is needed exactly as long as at least one of them. The disposal lifecycle now works in four places:
disposeInactiveEntriesis two-phase: staleENTRYrecords are marked first, the active set is computed after, and each child entry's disposal state is then recomputed from it — so orphaned children are scheduled in the same pass as their parents (a child that lagged a pass behind could survive a cleanup rebuild and persist indefinitely). The recompute is idempotent: parent links are kept, so revival is possible.handleAppDirPing,handlePing,ensurePageImpl) also clear matching disposed children viareviveChildEntriesFor, so a revived route never carries a child with a stale disposal flag — including into Rspack's built-entries cache.hot-reloader-webpack's entry computation (the site that deletes flagged entries) consults live parent state computed after the awaited entry resolution: a child whose parent was revived mid-build is kept even while it still carries a stale disposal flag.hot-reloader-rspackpersists childparentEntriesas arrays inbuilt-entries.jsonand rehydrates them intoSets on read (tolerating legacy caches that stored them as empty objects): the parent links now survive restarts, and disposal iteration can no longer crash on a plain-objectparentEntries.Only
ENTRYrecords count as parents: a child entry shares its parent's bundle path, so considering every record would keep the parent set alive through the child itself.Note on #89587
The pre-existing open PR #89587 attempts this fix but compares parent names against the bundle paths of all undisposed records including the child itself, which always matches (
parentEntriescontains the server entry name, which equals the child'sbundlePath), so it never disposes anything.Tests
packages/next/src/server/dev/on-demand-entry-handler.test.ts(pnpm jest packages/next/src/server/dev/on-demand-entry-handler.test.ts): same-pass parent+child disposal, removal of gone parents, empty parent sets, shared children with mixed parent states, synchronous + pass-based revival, active-parent computation, selective child revival, middleware/instrumentation exemptions. The disposal tests fail without the fix; 11/11 pass with it.maxInactiveAge, parents and children of three visited app pages were scheduled in the same pass and removed on the next compilation; re-visiting a disposed page revived both records synchronously and rendered fine.Fixes #54708