Skip to content

Attempt static prefetch before resorting to runtime - #96095

Merged
acdlite merged 1 commit into
canaryfrom
segment-prefetch-static-attempt
Jul 28, 2026
Merged

Attempt static prefetch before resorting to runtime#96095
acdlite merged 1 commit into
canaryfrom
segment-prefetch-static-attempt

Conversation

@acdlite

@acdlite acdlite commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

If we're reasonably confident that a segment can be prefetched statically without omitting data that would have been included during a runtime prefetch (e.g. cookies), the client should attempt prefetch the segment statically instead of going straight to a runtime request.

The decision for whether to do this is based on the ShouldAttemptStaticPrefetch added in previous steps.

If it turns out the static response is not sufficient, then it will fall back to a runtime request.

This makes prefetching cheaper for pages that are fully statically renderable. We can make the optimization better in the future with more reliable per-segment computation of the ShouldAttemptStaticPrefetch, but the current approach should at least work for fully static pages, which is what's most important.

This optimization applies during both the Shell phase and the Speculative phase of the prefetching algorithm.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 549 MB 550 MB 🔴 +1.09 MB (+0%) ▁▁▁▁▁
📊 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) 811ms 811ms ██▁▁█
Cold (Ready in log) 776ms 779ms ▄▄▃▂▃
Cold (First Request) 1.260s 1.268s ▂▃▂▂▂
Warm (Listen) 811ms 811ms ███▁█
Warm (Ready in log) 774ms 779ms ▃▄▃▃▅
Warm (First Request) 1.244s 1.263s ▂▃▁▇▃
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 813ms 813ms █▁█▁█
Cold (Ready in log) 785ms 798ms ▃▁▄▁▂
Cold (First Request) 3.486s 3.577s ▃▁▃▄▃
Warm (Listen) 812ms 813ms █▁███
Warm (Ready in log) 785ms 789ms ▅▁▂▂▂
Warm (First Request) 3.493s 3.530s ▃▁▁▃▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.009s 5.097s ▂▂▂▂▄
Cached Build 5.042s 5.067s ▂▅▂▁▂
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 24.214s 24.137s ▁▁▄▃▂
Cached Build 24.349s 24.432s ▄▁▄▄▂
node_modules Size 549 MB 550 MB 🔴 +1.09 MB (+0%) ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
00984xh3ankve.js gzip 7.4 kB N/A -
00lv61-dw-p1s.js gzip 71.2 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
0fkgqovmj3kd_.js gzip 156 B N/A -
0pt5of3ch6d2u.js gzip 13.6 kB N/A -
0s_6le5473zon.js gzip 153 B N/A -
0utq7k-24q3k_.js gzip 157 B N/A -
0x-zpe04zt6i5.js gzip 45.6 kB N/A -
0xnsdo-6y9r3k.js gzip 5.72 kB N/A -
0yi7is8u4covm.js gzip 8.77 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1prv-wg6jgtt2.js gzip 8.77 kB N/A -
1t47vod02e_4a.js gzip 8.75 kB N/A -
1tf1phijqlx9j.js gzip 220 B 220 B
1tw002sq5bo4l.js gzip 10 kB N/A -
1wlerik1n7zbr.js gzip 8.7 kB N/A -
2_zr4masp3rwy.js gzip 155 B N/A -
25xtago53_2xf.js gzip 160 B N/A -
2b2322zzwplhu.js gzip 161 B N/A -
2lviwqiqxm904.js gzip 3.52 kB N/A -
2pevdqjpbf3xk.js gzip 9.45 kB N/A -
2qd2anu9eizlz.js gzip 8.75 kB N/A -
2se72tcn7ulb_.js gzip 154 B N/A -
2sv39ae5_8vfj.js gzip 8.81 kB N/A -
2utx6n7w65xqa.js gzip 8.7 kB N/A -
2x2jbjxj8_k8h.js gzip 155 B N/A -
3-mgi7apkkmqn.js gzip 169 B N/A -
3-ukivglqkfeq.js gzip 8.78 kB N/A -
325xisqrylugu.js gzip 450 B N/A -
32fs4p58op4zm.js gzip 65.6 kB N/A -
367gc92a9294o.js gzip 158 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 -
3kyroy-o6ffl5.js gzip 154 B N/A -
3md__14nrd0du.js gzip 156 B N/A -
3qn7vntp30gaj.js gzip 1.47 kB N/A -
3yz9mvl23_vys.js gzip 156 B N/A -
turbopack-00..nycg.js gzip 3.86 kB N/A -
turbopack-03..h4gn.js gzip 3.85 kB N/A -
turbopack-04..7jl6.js gzip 3.84 kB N/A -
turbopack-07..hdpt.js gzip 3.85 kB N/A -
turbopack-07..6ba4.js gzip 3.84 kB N/A -
turbopack-0a..weg2.js gzip 3.84 kB N/A -
turbopack-0a..ncn4.js gzip 3.85 kB N/A -
turbopack-0b..vyhj.js gzip 3.84 kB N/A -
turbopack-16..dn94.js gzip 3.84 kB N/A -
turbopack-1a..ymrt.js gzip 3.84 kB N/A -
turbopack-20..b45f.js gzip 3.85 kB N/A -
turbopack-23..lykr.js gzip 3.82 kB N/A -
turbopack-3s..7_iy.js gzip 3.84 kB N/A -
turbopack-3y..s536.js gzip 3.84 kB N/A -
0_9-hrvr12n53.js gzip N/A 161 B -
030a1njeh5b77.js gzip N/A 10 kB -
04_0pje58tdal.js gzip N/A 153 B -
0762ohcfs5hmh.js gzip N/A 65.6 kB -
0ot0qt5np20h7.js gzip N/A 8.78 kB -
0t_fggm0_xxpv.js gzip N/A 156 B -
12m9rr7_7cexd.js gzip N/A 8.7 kB -
186r3y_rfvr0b.js gzip N/A 8.78 kB -
19kp1_j23-9g0.js gzip N/A 162 B -
19v97848yrjp7.js gzip N/A 8.81 kB -
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 -
1uojw_uabeg93.js gzip N/A 450 B -
1w-7lusgyl81y.js gzip N/A 8.7 kB -
1yf84zzm98oee.js gzip N/A 157 B -
21h56wdn9zddo.js gzip N/A 8.75 kB -
2fzbtehq6od5d.js gzip N/A 158 B -
2ia9jsnzvw1z_.js gzip N/A 158 B -
2lg10omv7xu14.js gzip N/A 3.52 kB -
2m7pysadpv9jx.js gzip N/A 170 B -
2ovff1533zvno.js gzip N/A 13.2 kB -
2s4z8jy0z7v93.js gzip N/A 1.46 kB -
2w9le8joqnx61.js gzip N/A 7.41 kB -
2y6gb5bs4mnam.js gzip N/A 8.75 kB -
30-xsnotaxkq2.js gzip N/A 155 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 -
3cm252vtfpe45.js gzip N/A 157 B -
3duxb6r04415w.js gzip N/A 156 B -
3euhi91ylya-8.js gzip N/A 157 B -
3oc7y32_eydsq.js gzip N/A 46.2 kB -
3ux8nb4tavd-x.js gzip N/A 71.2 kB -
3vdh6_u50pu5t.js gzip N/A 13.6 kB -
3xpdobk_3xyd8.js gzip N/A 156 B -
turbopack-0i..n_6b.js gzip N/A 3.85 kB -
turbopack-0u..57xm.js gzip N/A 3.84 kB -
turbopack-14..t3l7.js gzip N/A 3.85 kB -
turbopack-18..2rbz.js gzip N/A 3.82 kB -
turbopack-1i..1rnb.js gzip N/A 3.85 kB -
turbopack-1m..0fd7.js gzip N/A 3.85 kB -
turbopack-1o..7l0x.js gzip N/A 3.85 kB -
turbopack-1s..ysh5.js gzip N/A 3.85 kB -
turbopack-3-..zru6.js gzip N/A 3.85 kB -
turbopack-37..xvtz.js gzip N/A 3.85 kB -
turbopack-39..4wym.js gzip N/A 3.86 kB -
turbopack-3a..bncu.js gzip N/A 3.84 kB -
turbopack-3e..vk0g.js gzip N/A 3.85 kB -
turbopack-41..obw3.js gzip N/A 3.84 kB -
Total 449 kB 450 kB ⚠️ +673 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 775 B 774 B
Total 775 B 774 B ✅ -1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 432 B 436 B
Total 432 B 436 B ⚠️ +4 B

📦 Webpack

Client

Main Bundles
Canary PR Change
3322-HASH.js gzip 64.3 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 254 B 252 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 239 kB 241 kB ⚠️ +1.28 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 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 291 kB 293 kB
Total 420 kB 421 kB ⚠️ +892 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 618 B 617 B
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 45.3 kB 45.1 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 46.9 kB 46.7 kB ✅ -259 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.74 MB 4.82 MB 🔴 +82.9 kB (+2%)
index.pack gzip 123 kB 122 kB
index.pack.old gzip 124 kB 123 kB
Total 4.98 MB 5.06 MB ⚠️ +81.4 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 371 kB 372 kB
app-page-exp..prod.js gzip 205 kB 205 kB
app-page-tur...dev.js gzip 370 kB 371 kB
app-page-tur..prod.js gzip 205 kB 205 kB
app-page-tur...dev.js gzip 367 kB 368 kB
app-page-tur..prod.js gzip 202 kB 203 kB
app-page.run...dev.js gzip 367 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 55.9 kB 55.9 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.4 kB 45.4 kB
pages-api-tu..prod.js gzip 34.1 kB 34.1 kB
pages-api.ru...dev.js gzip 45.4 kB 45.4 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.7 kB 39.7 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 kB 72 kB
use-cache-pr...dev.js gzip 70.3 kB 70.3 kB
use-cache-pr...dev.js gzip 70.3 kB 70.3 kB
Total 4.07 MB 4.07 MB ⚠️ +6.36 kB
📝 Changed Files (10 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
  • pages-api.runtime.dev.js
  • pages.runtime.dev.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

pages-api.runtime.dev.js

Diff too large to display

pages.runtime.dev.js

Diff too large to display

📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/02d016d641649ebfc07ed2b163c2551785ee4bac/next

Commit: 02d016d

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 02d016d

@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 415fe8a to 4ff9530 Compare July 23, 2026 19:50
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 4ff9530 to c6bb39f Compare July 24, 2026 01:29
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch 3 times, most recently from 544b1b3 to 060b848 Compare July 24, 2026 18:27
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 060b848 to 810e43a Compare July 25, 2026 01:09
@acdlite
acdlite marked this pull request as ready for review July 25, 2026 01:19
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 810e43a to 6956d9c Compare July 25, 2026 02:54
@acdlite
acdlite requested a review from unstubbable July 25, 2026 02:55
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch 2 times, most recently from 00333fa to a8c842c Compare July 25, 2026 04:38
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch 2 times, most recently from 3de424e to 77143cd Compare July 27, 2026 03:27
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch 2 times, most recently from f6f7a0f to ce40d01 Compare July 27, 2026 04:27
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch 2 times, most recently from 91c936b to 325ff7b Compare July 27, 2026 17:36
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 325ff7b to 11213ee Compare July 27, 2026 21:53
Base automatically changed from segment-prefetch-block-until-response to canary July 27, 2026 23:03
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 11213ee to cdffd14 Compare July 27, 2026 23:04
// post-shell runtime-data access doesn't mark the shell variant itself as
// needing a runtime request.
// (An offset that somehow isn't visible in the decode reads as 0 — no
// shell — the conservative direction: it can waste a runtime request but

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment is a bit contradictory with:

// stranded blocking on them. Note the scheduler does NOT fall back to
// a runtime request for rejected segments — it skips them outright (see

Comment on lines +2771 to +2783
// When this payload fully satisfied the segment — no runtime request
// needed — the content is as complete as a RUNTIME response of the same
// variant would have been, so it records that runtime tier. That's what
// lets the scheduler decide "would a runtime request return more?" by
// comparing tiers alone, with no separate signal to consult.
//
// Otherwise the content is only as complete as the static tier it was
// requested at, so a follow-up runtime request can still supersede it.
const recordedFetchStrategy = !needsRuntimeRequest
? payloadFetchStrategy === FetchStrategy.StaticShell
? FetchStrategy.RuntimeShell
: FetchStrategy.PPRRuntime
: fetchStrategy

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.

hmm, this part feels a little weird. having to pretend that we used a different strategy makes me feel like there's some nicer way to model this. although i suppose it makes sense if we think about the strategies as "level of completeness"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah my plan is to rename the field something like "completeness" but have it represent both sides

@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from cdffd14 to 0cd482a Compare July 28, 2026 14:26
If we're reasonably confident that a segment can be prefetched statically
without omitting data that would have been included during a runtime prefetch
(e.g. cookies), the client should attempt prefetch the segment statically
instead of going straight to a runtime request.

The decision for whether to do this is based on the ShouldAttemptStaticPrefetch
added in previous steps.

If it turns out the static response is not sufficient, then it will fall back
to a runtime request.

This makes prefetching cheaper for pages that are fully statically renderable.
We can make the optimization better in the future with more reliable per-segment
computation of the ShouldAttemptStaticPrefetch, but the current approach should
at least work for fully static pages, which is what's most important.

This optimization applies during both the Shell phase and the Speculative phase
of the prefetching algorithm.
@acdlite
acdlite force-pushed the segment-prefetch-static-attempt branch from 0cd482a to 02d016d Compare July 28, 2026 15:22
@acdlite
acdlite merged commit dfa7f4f into canary Jul 28, 2026
224 of 226 checks passed
@acdlite
acdlite deleted the segment-prefetch-static-attempt branch July 28, 2026 15:51
aurorascharff added a commit that referenced this pull request Jul 28, 2026
…ching (#96302)

### What?

Update the interactive apps guide's prefetching section for unified
runtime prefetching.

### Why?

#96106 removed the `allow-runtime` segment config, and the guide's
section still described the old model after the mechanical rename: it
told readers session data (cookies, headers) needs the link opt-in, when
the App Shell already carries session-gated UI. Only URL data needs
`prefetch={true}`.

### How?

Rework the section around the per-link model: the shell carries static,
cached, and session output, `prefetch={true}` resolves URL data ahead of
the click, and the static prefetch attempt (#96095) makes the server
cost an internal optimization. Includes a technical-writing pass on the
section (full-noun subjects, split splices) and aligns the Next steps
table row.
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.

3 participants