Skip to content

Gate partial fallback shell upgrades behind partialPrefetching for next start - #96297

Merged
acdlite merged 1 commit into
canaryfrom
partial-fallback-next-start
Jul 29, 2026
Merged

Gate partial fallback shell upgrades behind partialPrefetching for next start#96297
acdlite merged 1 commit into
canaryfrom
partial-fallback-next-start

Conversation

@acdlite

@acdlite acdlite commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

In #96074 I put the partialFallback behavior behind the partialPrefetching flag, so that we don't risk an explosion in ISR costs for apps that haven't opted into Partial Prefetching. But it turns out I only handled this for deploy mode — i.e. the build output consumed by the Vercel adapter. The behavior was still unconditionally on for next start.

The reason it was easy to miss is that next start and the adapter express partial fallback shells through completely separate mechanisms. The adapter emits a partialFallback flag into the build output and lets the platform's ISR layer perform the shell upgrade. next start has no such flag; the server performs the upgrade itself, in the compiled page runtime. So the gate I added to the adapter output had no effect on the self-hosted path.

To understand how the next start path was configured, I looked at the PR that removed the original top-level partialFallbacks config flag (#93859). Before that PR, the behavior was gated in the runtime; when the flag was removed those gates were deleted and the behavior became always-on.

The important subtlety is that only the upgrade should be gated, not the shell machinery as a whole. On next start, the value that decides whether a shell can be specialized (remainingPrerenderableParams) also drives core Cache Components behavior — build-time partial prerendering of params and serving build-time sub-shells — which must stay on regardless of partialPrefetching. So this gates only the two things that make up the "upgrade on first request" cost risk: the background ISR revalidation that specializes a shell per request, and the client-facing isFallbackUpgradeable signal that tells the client to retry a prefetch waiting for that upgrade.

Adds coverage with a partialPrefetching-disabled fixture asserting that fallback shells stay shared rather than specializing per request.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 8739f7c

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 813ms 813ms █████
Cold (Ready in log) 800ms 792ms █▄▃▅▄
Cold (First Request) 1.339s 1.327s █▃▂▃▂
Warm (Listen) 813ms 813ms ███▁█
Warm (Ready in log) 798ms 795ms █▆▄▂▅
Warm (First Request) 1.318s 1.309s █▃▃▄▂
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 760ms 812ms ███▁█
Cold (Ready in log) 763ms 776ms ▆▄█▂█
Cold (First Request) 3.435s 3.491s ▁█▁▁▁
Warm (Listen) 810ms 812ms ███▁█
Warm (Ready in log) 768ms 777ms ▅▅█▂▇
Warm (First Request) 3.427s 3.448s ▃▅█▃▇

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.603s 5.384s █▃▃▄▃
Cached Build 5.653s 5.474s █▃▃▅▂
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 24.034s 24.096s ▂▅█▂▇
Cached Build 24.073s 23.867s ▂▄█▂█
node_modules Size 551 MB 551 MB ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
02vc1wabfodef.js gzip 7.4 kB N/A -
0a-p0meqxynjz.js gzip 10.3 kB N/A -
0avtke4yefj4-.js gzip 13.1 kB N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0d-r2ds9o2ph-.js gzip 160 B N/A -
0f75he3zfr9e4.js gzip 65.6 kB N/A -
0kywi74zio6hr.js gzip 156 B N/A -
0pt5of3ch6d2u.js gzip 13.6 kB N/A -
0xnsdo-6y9r3k.js gzip 5.72 kB N/A -
0yi7is8u4covm.js gzip 8.77 kB N/A -
1_wvjeged7lg_.js gzip 155 B N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1ju2ri7wd2c3o.js gzip 46.2 kB N/A -
1kzalatw2db0g.js gzip 154 B N/A -
1l5zfgc6a6f1v.js gzip 158 B N/A -
1prv-wg6jgtt2.js gzip 8.77 kB N/A -
1t47vod02e_4a.js gzip 8.75 kB N/A -
1tc-wat3789me.js gzip 156 B N/A -
1tf1phijqlx9j.js gzip 220 B 220 B
1tw002sq5bo4l.js gzip 10 kB N/A -
1wlerik1n7zbr.js gzip 8.7 kB N/A -
1x44sfndh633_.js gzip 169 B N/A -
281w4uo0qh5-y.js gzip 155 B N/A -
2bg1bjhh_85w_.js gzip 71.2 kB N/A -
2lviwqiqxm904.js gzip 3.52 kB N/A -
2pevdqjpbf3xk.js gzip 9.45 kB N/A -
2po5hui9ncxet.js gzip 156 B N/A -
2px0someq33rr.js gzip 159 B N/A -
2qd2anu9eizlz.js gzip 8.75 kB N/A -
2sv39ae5_8vfj.js gzip 8.81 kB N/A -
2utx6n7w65xqa.js gzip 8.7 kB N/A -
3-ukivglqkfeq.js gzip 8.78 kB N/A -
325xisqrylugu.js gzip 450 B N/A -
3jc9ghvds1spf.js gzip 2.29 kB N/A -
3juwkvkgvywfe.js gzip 10.6 kB N/A -
3kbove7q58uqh.js gzip 13.2 kB N/A -
3oqs4pmeqymlx.js gzip 156 B N/A -
3q1u2x9az6_oa.js gzip 157 B N/A -
3qn7vntp30gaj.js gzip 1.47 kB N/A -
4075wqjatg7h6.js gzip 153 B N/A -
turbopack-0_..x1ji.js gzip 3.82 kB N/A -
turbopack-0c..y0og.js gzip 3.84 kB N/A -
turbopack-0e..1gw_.js gzip 3.84 kB N/A -
turbopack-0m..iqeg.js gzip 3.84 kB N/A -
turbopack-13..pv_8.js gzip 3.84 kB N/A -
turbopack-1i..m_40.js gzip 3.84 kB N/A -
turbopack-2u..t2kt.js gzip 3.85 kB N/A -
turbopack-30..qza0.js gzip 3.84 kB N/A -
turbopack-34..gzrg.js gzip 3.84 kB N/A -
turbopack-35..cup2.js gzip 3.85 kB N/A -
turbopack-3g..ch7d.js gzip 3.84 kB N/A -
turbopack-3l..0n4i.js gzip 3.86 kB N/A -
turbopack-3n..tay_.js gzip 3.84 kB N/A -
turbopack-3y.._bcc.js gzip 3.84 kB N/A -
0_tqu_e2e9mt4.js gzip N/A 156 B -
0-f6fw94idoz5.js gzip N/A 156 B -
030a1njeh5b77.js gzip N/A 10 kB -
03dlk9whcd-oh.js gzip N/A 157 B -
04cymg_4nf3a9.js gzip N/A 46.2 kB -
0jx_1rd8v13jz.js gzip N/A 157 B -
0ot0qt5np20h7.js gzip N/A 8.78 kB -
12m9rr7_7cexd.js gzip N/A 8.7 kB -
186r3y_rfvr0b.js gzip N/A 8.78 kB -
19v97848yrjp7.js gzip N/A 8.81 kB -
1e1foewnpl-z2.js gzip N/A 155 B -
1fn50kpu5fxi8.js gzip N/A 155 B -
1k4ado9z38f28.js gzip N/A 158 B -
1nd_0rwgz2ozk.js gzip N/A 9.45 kB -
1oi9pltr_e10m.js gzip N/A 10.6 kB -
1rd9tzqgzsz2w.js gzip N/A 8.77 kB -
1rq1g_cwv642d.js gzip N/A 10.3 kB -
1u46k8h0er_rg.js gzip N/A 154 B -
1uojw_uabeg93.js gzip N/A 450 B -
1w-7lusgyl81y.js gzip N/A 8.7 kB -
2-p9j-2qibdpi.js gzip N/A 157 B -
20cxbvi52dcu8.js gzip N/A 7.41 kB -
215wupwyojw2k.js gzip N/A 71.2 kB -
21h56wdn9zddo.js gzip N/A 8.75 kB -
2lg10omv7xu14.js gzip N/A 3.52 kB -
2ovff1533zvno.js gzip N/A 13.2 kB -
2s4z8jy0z7v93.js gzip N/A 1.46 kB -
2y6gb5bs4mnam.js gzip N/A 8.75 kB -
3-z819ovdu4xt.js gzip N/A 161 B -
341h71ln1jpl3.js gzip N/A 2.29 kB -
34ttpt_n7605h.js gzip N/A 5.72 kB -
36ckfq9o-ys-a.js gzip N/A 13.1 kB -
38209birsk866.js gzip N/A 65.6 kB -
3csoe345wjrt1.js gzip N/A 157 B -
3ecqyzcgreqn4.js gzip N/A 162 B -
3tw46-f-254s_.js gzip N/A 169 B -
3vdh6_u50pu5t.js gzip N/A 13.6 kB -
turbopack-0e..nd1b.js gzip N/A 3.84 kB -
turbopack-0j..yyyy.js gzip N/A 3.85 kB -
turbopack-0w..45op.js gzip N/A 3.85 kB -
turbopack-14..0nue.js gzip N/A 3.84 kB -
turbopack-1a..y1x7.js gzip N/A 3.85 kB -
turbopack-1d..ifqu.js gzip N/A 3.85 kB -
turbopack-1w..l8eo.js gzip N/A 3.85 kB -
turbopack-21..-5be.js gzip N/A 3.84 kB -
turbopack-25..g87v.js gzip N/A 3.85 kB -
turbopack-2m..ystt.js gzip N/A 3.82 kB -
turbopack-2t..qrfm.js gzip N/A 3.86 kB -
turbopack-3e..9xqv.js gzip N/A 3.85 kB -
turbopack-3v..asox.js gzip N/A 3.84 kB -
turbopack-42..wlmy.js gzip N/A 3.84 kB -
Total 450 kB 450 kB ⚠️ +70 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 775 B 775 B
Total 775 B 775 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 428 B 433 B 🔴 +5 B (+1%)
Total 428 B 433 B ⚠️ +5 B

📦 Webpack

Client

Main Bundles
Canary PR Change
3322-HASH.js gzip 64.9 kB N/A -
4191.HASH.js gzip 169 B N/A -
7920-HASH.js gzip 4.68 kB N/A -
9784-HASH.js gzip 5.63 kB N/A -
b1ad9f4c-HASH.js gzip 63 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 253 B 253 B
main-HASH.js gzip 40 kB 40 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
3577.HASH.js gzip N/A 168 B -
578-HASH.js gzip N/A 65.6 kB -
8590-HASH.js gzip N/A 5.61 kB -
9750-HASH.js gzip N/A 4.69 kB -
a8984546-HASH.js gzip N/A 63 kB -
Total 240 kB 241 kB ⚠️ +667 B
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 193 B
_error-HASH.js gzip 181 B 182 B
css-HASH.js gzip 334 B 331 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 253 B
head-HASH.js gzip 349 B 351 B
hooks-HASH.js gzip 382 B 384 B
image-HASH.js gzip 581 B 582 B
index-HASH.js gzip 260 B 259 B
link-HASH.js gzip 2.48 kB 2.48 kB
routerDirect..HASH.js gzip 317 B 318 B
script-HASH.js gzip 384 B 386 B
withRouter-HASH.js gzip 316 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.95 kB 7.95 kB ⚠️ +1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 129 kB 128 kB
page.js gzip 292 kB 293 kB
Total 421 kB 421 kB ⚠️ +308 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 616 B 619 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 45.6 kB 45.5 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 47.3 kB 47.2 kB ✅ -99 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 718 B 718 B
Total 718 B 718 B
Build Cache
Canary PR Change
0.pack gzip 4.82 MB 4.82 MB
index.pack gzip 123 kB 123 kB
index.pack.old gzip 122 kB 123 kB
Total 5.07 MB 5.07 MB ✅ -1.61 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 372 kB 372 kB
app-page-exp..prod.js gzip 205 kB 205 kB
app-page-tur...dev.js gzip 372 kB 372 kB
app-page-tur..prod.js gzip 205 kB 205 kB
app-page-tur...dev.js gzip 368 kB 368 kB
app-page-tur..prod.js gzip 203 kB 203 kB
app-page.run...dev.js gzip 368 kB 368 kB
app-page.run..prod.js gzip 203 kB 203 kB
app-route-ex...dev.js gzip 82.5 kB 82.5 kB
app-route-ex..prod.js gzip 55.9 kB 55.9 kB
app-route-tu...dev.js gzip 82.5 kB 82.5 kB
app-route-tu..prod.js gzip 56 kB 56 kB
app-route-tu...dev.js gzip 82.1 kB 82.1 kB
app-route-tu..prod.js gzip 55.7 kB 55.7 kB
app-route.ru...dev.js gzip 82.1 kB 82.1 kB
app-route.ru..prod.js gzip 55.7 kB 55.7 kB
dev-validati...dev.js gzip 132 kB 132 kB
dev-validati...dev.js gzip 132 kB 132 kB
dev-validati...dev.js gzip 130 kB 130 kB
dev-validati...dev.js gzip 130 kB 130 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 45.5 kB 45.5 kB
pages-api-tu..prod.js gzip 34.1 kB 34.1 kB
pages-api.ru...dev.js gzip 45.5 kB 45.5 kB
pages-api.ru..prod.js gzip 34.1 kB 34.1 kB
pages-turbo....dev.js gzip 54.9 kB 54.9 kB
pages-turbo...prod.js gzip 39.8 kB 39.8 kB
pages.runtim...dev.js gzip 54.9 kB 54.9 kB
pages.runtim..prod.js gzip 39.8 kB 39.8 kB
server.runti..prod.js gzip 68.3 kB 68.3 kB
use-cache-pr...dev.js gzip 72 kB 72 kB
use-cache-pr...dev.js gzip 72.1 kB 72.1 kB
use-cache-pr...dev.js gzip 70.4 kB 70.4 kB
use-cache-pr...dev.js gzip 70.4 kB 70.4 kB
Total 4.08 MB 4.08 MB ✅ -1 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/8739f7caf36026c0d68f9ffa6a23aa9c58ab966e/next

Commit: 8739f7c

@acdlite
acdlite force-pushed the partial-fallback-next-start branch 2 times, most recently from ec56528 to 8a04a01 Compare July 28, 2026 20:12
@acdlite acdlite changed the title Gate partial fallback shells behind partialPrefetching for next start Gate partial fallback shell upgrades behind partialPrefetching for next start Jul 28, 2026
@acdlite
acdlite force-pushed the partial-fallback-next-start branch 2 times, most recently from fd9aa37 to 13c3227 Compare July 29, 2026 17:08
@acdlite
acdlite marked this pull request as ready for review July 29, 2026 18:08
@acdlite
acdlite requested a review from ztanner July 29, 2026 18:08
}, 'no-requests')
})

it('retries a static fallback prefetch a bounded number of times, then stops', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this test run under a fixture with partialPrefetching: true? With the current config, isUpgradeableISRFallback should always be false, so the retry loop never starts and the < 5 assertion can pass with only the initial request.

…`next start`

In #96074 I put the `partialFallback` behavior behind the
`partialPrefetching` flag, so that we don't risk an explosion in ISR
costs for apps that haven't opted into Partial Prefetching. But it turns
out I only handled this for deploy mode — i.e. the build output consumed
by the Vercel adapter. The behavior was still unconditionally on for
`next start`.

The reason it was easy to miss is that `next start` and the adapter
express partial fallback shells through completely separate mechanisms.
The adapter emits a `partialFallback` flag into the build output and lets
the platform's ISR layer perform the shell upgrade. `next start` has no
such flag; the server performs the upgrade itself, in the compiled page
runtime. So the gate I added to the adapter output had no effect on the
self-hosted path.

For `next start` the behavior we want is exactly what existed before the
`experimental.partialFallbacks` config flag was removed in #93859. That
PR deleted a coherent set of gates — the build-time shell metadata plus
the runtime cache-key, fallback-mode, and background-upgrade decisions —
and made the behavior always-on. This restores those same gates, keyed on
`partialPrefetching` instead of the deleted flag. When Partial Prefetching
is off, a fallback shell is served from the shared route shell (the normal
ISR cache key) and never specialized or upgraded per request; when it's
on, the shell specializes and upgrades exactly as it does today.

The gates have to be restored as a set. Gating only the background upgrade
(and leaving the cache key still specializing the shell) writes the
specialized entry under a key that nothing ever populates, which breaks
`revalidatePath` / `revalidateTag` for the affected routes. Restoring the
cache-key gate alongside it keeps the entry on the normal ISR path.

Tests: adds a `partialPrefetching`-disabled fixture asserting fallback
shells stay shared, and updates the `next start` suites that exercise the
upgrade to opt into `partialPrefetching` so they keep testing it:
`sub-shell-generation-middleware` (only in its Cache Components mode, since
`partialPrefetching` requires Cache Components), `cache-components-allow-
otel-spans`, and `prefetch-fallback-retry`. The last also sets
`prefetch={true}` on its links, because a Partial Prefetching app skips
the speculative prefetch otherwise — without it the client never retries
and the bounded-retry assertion would pass vacuously.
@acdlite
acdlite force-pushed the partial-fallback-next-start branch from 13c3227 to 8739f7c Compare July 29, 2026 19:27
@acdlite
acdlite enabled auto-merge (squash) July 29, 2026 19:50
@acdlite
acdlite merged commit 3717cd3 into canary Jul 29, 2026
131 checks passed
@acdlite
acdlite deleted the partial-fallback-next-start branch July 29, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants