Only announce server component changes when compiled output changed - #96783
Closed
gaearon wants to merge 2 commits into
Closed
Only announce server component changes when compiled output changed#96783gaearon wants to merge 2 commits into
gaearon wants to merge 2 commits into
Conversation
Contributor
Stats from current PR🔴 3 regressions
📊 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
📝 Changed Files (4 files)Files with changes:
View diffspages-api-tu..ntime.dev.jsDiff too large to display pages-turbo...ntime.dev.jsDiff too large to display pages-api.runtime.dev.jsDiff too large to display pages.runtime.dev.jsDiff too large to display 📎 Tarball URLCommit: 92279da |
Contributor
Failing test suitesCommit: 92279da | About building and testing Next.js
Expand output● app dir - navigation › middleware redirect › should change browser location when router.refresh() gets a redirect response |
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
2 times, most recently
from
August 6, 2026 01:50
4b7b46e to
d738b8f
Compare
gaearon
marked this pull request as ready for review
August 6, 2026 01:51
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 02:33
d738b8f to
f21feef
Compare
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 03:18
f21feef to
1509660
Compare
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 03:23
1509660 to
c34972e
Compare
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 03:56
c34972e to
4afac2f
Compare
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 04:02
4afac2f to
850fdab
Compare
gaearon
requested review from
bgw,
Copilot and
wbinnssmith
and removed request for
Copilot
August 6, 2026 04:09
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 08:03
850fdab to
43d4532
Compare
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
2 times, most recently
from
August 6, 2026 09:00
8024ca8 to
a54aaf6
Compare
Adding or removing a page changes no other page's server components, so an open tab must refetch exactly once per addition (the added-page announcement) and once per removal (the removed-page announcement), with no serverComponentChanges announcement for either. This extends the counted refetch test with a removal phase and pins the announcement itself. On Turbopack the removal half fails: removing a page that has been built applies a server HMR update, and applying any update announces changed server components, so every tab is told to refetch again for a removal that changed nothing it renders. Whether that shows up as a third refetch or folds into the announcement's refetch depends on how close together the two messages reach the browser, so counting fetches alone can't pin it down — asserting on the announcement makes the failure deterministic. An edit to the page the tab is showing must still announce, exactly once, with exactly one more refetch. Besides pinning the edit path, this keeps the zero-announcements assertion honest: if the capture ever stops seeing the messages, the edit phase fails instead of the empty check passing vacuously.
Applying a server HMR update ended with telling every connected tab to refetch its RSC payload. But an applied update doesn't necessarily change what any page renders: removing a page that the server has built applies an update that rewrites the removed module into a throwing stub inside the merged server chunks, and no other entry's output changes. Every tab still refetched for it — and since the removed-page announcement makes tabs refetch already, a removal cost each tab two refetches (or one, depending on how close together the two messages arrived, which is what made the refetch-count test unpinnable). The apply loop now decides the way the HMR refresh hash already does: compare each subscribed server entry's compiled output hash against a baseline, and announce only when one differs. Baselines are seeded when an entry's change subscription starts, before the entry's first render is underway (an edit landing in the milliseconds between those two can be absorbed into the baseline and miss one announcement — the same window the refresh-hash baseline a PR down already has, corrected by the next change either way). A null hash (the entry was removed, or currently fails to build) keeps the previous baseline, so output coming back different is still announced. A hash that can't be read at all counts as changed — the announcement is what every apply sent unconditionally before this gate. Env-change invalidations announce through their own path and are unaffected. This flips the previous commit's test green on Turbopack: an open tab now refetches exactly once per page addition and once per removal, with no server-component-changes announcement for either, matching webpack. Also still green: server-components-hmr-cache (17 — edits and env changes must still refetch and invalidate), middleware-dev-update (4), the basic hmr, error-recovery, and full-reload suites (4, 12, 2), the use cache spurious-invalidation suite (6), and the full route-change-refetch suite (10).
gaearon
force-pushed
the
gaearon-server-hmr-deletion-announcements
branch
from
August 6, 2026 12:46
a54aaf6 to
92279da
Compare
Contributor
Author
|
May revisit this in the future, for now don't want to spend more time on it |
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.
Stacked on #96248.
In dev with Turbopack, applying a server HMR update ends with telling every connected tab to refetch its RSC payload. But an applied update doesn't necessarily change anything a tab renders. Removing a page is the clearest case: the update just rewrites the removed page's module into a throwing stub inside the merged server chunks —
— and no other page's output changes. Every tab was still told to refetch. Since the removed-page announcement already makes tabs refetch, deleting one page made every tab refetch twice instead of once.
Messages the server sends for
rm app/zz-added/page.tsxwhile a tab shows/counted, before the fix:after:
Because the two messages arrive within milliseconds, the browser sometimes handles them as one refetch and sometimes as two, depending on the machine. This is why the refetch-count test could not pin down the removal: idle machines usually saw two fetches, CI machines usually saw one, and the test expected an exact number. That was the
Expected: 3, Received: 2failure every Turbopack CI job showed for the PRs below this one. Since no exact refetch count is correct while the extra announcement exists, the test now also asserts on the announcement itself, which doesn't depend on timing: without this fix it fails every run, on every machine.The apply loop now decides the same way the
"use cache"refresh hash already does one PR down: compare each subscribed entry's compiled-output hash (serverContentHash()from #96248) against a baseline, and announce only when one differs.Promise.allSettledbecause a rejected read must not take down the apply loop — its failure path re-evaluates every server module.Test plan
The first commit extends the refetch-count test with a removal phase — exactly one refetch per addition and one per removal, on both bundlers — and asserts that no
serverComponentChangesis announced for either. The test ends by checking the opposite direction too: editing the page the tab is showing must produce exactly oneserverComponentChangesand exactly one more refetch. Besides covering the edit path, this proves the message capture works — if the capture silently broke, the "no announcements" assertions would pass without checking anything.Without the fix, the test fails on Turbopack 3 of 3 runs; webpack passes it unchanged. With the fix, Turbopack passes 3 of 3, and an earlier revision of the same test (without the edit phase) passed 30 consecutive full-suite runs.
Suites covering the changed loop, all green with the fix (locally built binding)
route-change-refetch— 30 consecutive Turbopack runs of the counted test green; webpack 3×10/10. The pre-existing "tab stuck on 404 after a page add" flake (the ensurePage/watcher race, being fixed separately) is unaffected: matched warm runs are 10/10 both with and without this fix.server-components-hmr-cache(17) — edits and env changes must still refetch and invalidatemiddleware-dev-update(4)hmrbasic (4), error-recovery (12 + 1 skipped), full-reload (2)cache-components-spurious-cache-invalidation(6, idle and under CPU load)dynamic-optional-routingunder experimental Turbopack (33)