Skip to content

Block prefetch task until sufficient response is received#96017

Draft
acdlite wants to merge 1 commit into
segment-prefetch-needs-runtime-requestfrom
segment-prefetch-block-until-response
Draft

Block prefetch task until sufficient response is received#96017
acdlite wants to merge 1 commit into
segment-prefetch-needs-runtime-requestfrom
segment-prefetch-block-until-response

Conversation

@acdlite

@acdlite acdlite commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

This is a refactor of the prefetching algorithm to block the prefetch task from completing until the response is received.

Currently, a prefetch task is fire-and-forget: once a prefetch's requests are spawned, the scheduler marks the originating task as complete regardless of what the server ends up returning. However, certain planned optimizations require us to verify the response from the server to determine whether additional work is necessary. For example, we may attempt to do a static prefetch instead of a runtime prefetch if we optimistically assume that the static prefetch does not vary on runtime data. We need to verify that assumption is correct before marking the task as complete.

Now, whenever a pass over the task spawns a segment request, or encounters an entry whose response hasn't arrived yet, the task registers itself on the entry and exits as Blocked instead of Done. When the entry resolves, the blocked task is pinged and the pass re-runs against the received data. Only a pass that observes every response it cares about advances the task to the next phase or completes it.

Rejected entries are handled per segment: a rejection counts as an observed response, so the pass skips that segment and keeps prefetching the rest. The task never registers on a rejected entry, since nothing ever pings one. Retries are governed by the existing per-entry logic.

Also fixes a cache bug this exposed: when a revalidation response is keyed at a more generic vary path than the stale partial entry that prompted it (because the segment turned out not to vary on some param), the stale entry shadowed the result on every lookup — the upgraded data was unreachable and the revalidation was wasted. With blocking, that dead end became an infinite loop: each fulfillment re-pinged the blocked task, which re-read the same stale entry and spawned another revalidation. Upserting (or re-keying) a segment now deletes settled entries at more specific vary paths that the incoming entry supersedes.

The router-act test helper's "at least one request initiated" watchdog is adjusted to account for the new blocking behavior: countable requests can now be legitimately gated on an in-flight App Shell response (which the watchdog intentionally doesn't count). On expiry it now proceeds to response processing if the act scope intercepted a shell request, and keeps waiting while shell requests are in flight elsewhere on the page, instead of timing out.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Failing test suites

Commit: c53c7fa | About building and testing Next.js

pnpm test-start test/e2e/app-dir/cache-components-errors/client-hook-abort-reasons.test.ts (job)

  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > normal aborting > should capture useSearchParams after the data slot (DD)
  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > normal aborting > should capture usePathname before the data slot (DD)
  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > normal aborting > should capture useParams after the data slot (DD)
  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > normal aborting > should capture useSelectedLayoutSegments before the data slot (DD)
  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > normal aborting > should capture useSelectedLayoutSegment after the data slot (DD)
  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > sync IO aborting > should capture useParams before sync IO and data (DD)
  • Cache Components Errors - Client Hook Abort Reasons > Build With --debug-prerender > focused client hook abort reasons > sync IO aborting > should capture data and useSelectedLayoutSegment before sync IO (DD)
Expand output

● Cache Components Errors - Client Hook Abort Reasons › Build With --debug-prerender › focused client hook abort reasons › normal aborting › should capture useSearchParams after the data slot

expect(received).toMatchInlineSnapshot(snapshot)

Snapshot name: `Cache Components Errors - Client Hook Abort Reasons Build With --debug-prerender focused client hook abort reasons normal aborting should capture useSearchParams after the data slot 1`

- Snapshot  - 6
+ Received  + 6

@@ -27,17 +27,17 @@
  Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook
      at Object.useDynamicSearchParams (webpack:///<next-src>)
      at useSearchParams (webpack:///<next-src>)

  at UseSearchParams (../webpack:/app/client-hook-abort-reasons/client.tsx:27:18)
  at Page (../webpack:/app/client-hook-abort-reasons/normal/use-search-params/[id]/page.tsx:8:7)
  -   718 |       return
  +   728 |       return
  -   719 |     case 'prerender-client': {
  +   729 |     case 'prerender-client': {
  - > 720 |       React.use(
  + > 730 |       React.use(
          |             ^
  -   721 |         makeClientHookHangingPromise(
  +   731 |         makeClientHookHangingPromise(
  -   722 |           workUnitStore.renderSignal,
  +   732 |           workUnitStore.renderSignal,
  -   723 |           new ClientHookDynamicError(workStore.route, expression) {
  +   733 |           new ClientHookDynamicError(workStore.route, expression) {
      digest: 'CLIENT_HOOK_DYNAMIC'
    }
    To debug the issue, start the app in development mode by running `next dev`, then open "/client-hook-abort-reasons/normal/use-search-params/[id]" in your browser to investigate the error.
    Error occurred prerendering page "/client-hook-abort-reasons/normal/use-search-params/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
    ↵
  at Object.toMatchInlineSnapshot (e2e/app-dir/cache-components-errors/client-hook-abort-reasons.test.ts:289:32)

● Cache Components Errors - Client Hook Abort Reasons › Build With --debug-prerender › focused client hook abort reasons › normal aborting › should capture usePathname before the data slot

expect(received).toMatchInlineSnapshot(snapshot)

Snapshot name: `Cache Components Errors - Client Hook Abort Reasons Build With --debug-prerender focused client hook abort reasons normal aborting should capture usePathname before the data slot 1`

- Snapshot  - 6
+ Received  + 6

@@ -27,17 +27,17 @@
  Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook
      at Object.useDynamicRouteParams (webpack:///<next-src>)
      at usePathname (webpack:///<next-src>)

  at UsePathname (../webpack:/app/client-hook-abort-reasons/client.tsx:22:14)
  at Page (../webpack:/app/client-hook-abort-reasons/normal/use-pathname/[id]/page.tsx:7:7)
  -   647 |           // hang here and never resolve. This will cause the currently
  +   657 |           // hang here and never resolve. This will cause the currently
  -   648 |           // rendering component to effectively be a dynamic hole.
  +   658 |           // rendering component to effectively be a dynamic hole.
  - > 649 |           React.use(
  + > 659 |           React.use(
          |                 ^
  -   650 |             makeClientHookHangingPromise(
  +   660 |             makeClientHookHangingPromise(
  -   651 |               workUnitStore.renderSignal,
  +   661 |               workUnitStore.renderSignal,
  -   652 |               new ClientHookDynamicError(workStore.route, expression) {
  +   662 |               new ClientHookDynamicError(workStore.route, expression) {
      digest: 'CLIENT_HOOK_DYNAMIC'
    }
    To debug the issue, start the app in development mode by running `next dev`, then open "/client-hook-abort-reasons/normal/use-pathname/[id]" in your browser to investigate the error.
    Error occurred prerendering page "/client-hook-abort-reasons/normal/use-pathname/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
    ↵
  at Object.toMatchInlineSnapshot (e2e/app-dir/cache-components-errors/client-hook-abort-reasons.test.ts:579:32)

● Cache Components Errors - Client Hook Abort Reasons › Build With --debug-prerender › focused client hook abort reasons › normal aborting › should capture useParams after the data slot

expect(received).toMatchInlineSnapshot(snapshot)

Snapshot name: `Cache Components Errors - Client Hook Abort Reasons Build With --debug-prerender focused client hook abort reasons normal aborting should capture useParams after the data slot 1`

- Snapshot  - 6
+ Received  + 6

@@ -27,17 +27,17 @@
  Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook
      at Object.useDynamicRouteParams (webpack:///<next-src>)
      at useParams (webpack:///<next-src>)

  at UseParams (../webpack:/app/client-hook-abort-reasons/client.tsx:17:12)
  at Page (../webpack:/app/client-hook-abort-reasons/normal/use-params/[id]/page.tsx:8:7)
  -   647 |           // hang here and never resolve. This will cause the currently
  +   657 |           // hang here and never resolve. This will cause the currently
  -   648 |           // rendering component to effectively be a dynamic hole.
  +   658 |           // rendering component to effectively be a dynamic hole.
  - > 649 |           React.use(
  + > 659 |           React.use(
          |                 ^
  -   650 |             makeClientHookHangingPromise(
  +   660 |             makeClientHookHangingPromise(
  -   651 |               workUnitStore.renderSignal,
  +   661 |               workUnitStore.renderSignal,
  -   652 |               new ClientHookDynamicError(workStore.route, expression) {
  +   662 |               new ClientHookDynamicError(workStore.route, expression) {
      digest: 'CLIENT_HOOK_DYNAMIC'
    }
    To debug the issue, start the app in development mode by running `next dev`, then open "/client-hook-abort-reasons/normal/use-params/[id]" in your browser to investigate the error.
    Error occurred prerendering page "/client-hook-abort-reasons/normal/use-params/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
    ↵
  at Object.toMatchInlineSnapshot (e2e/app-dir/cache-components-errors/client-hook-abort-reasons.test.ts:869:32)

● Cache Components Errors - Client Hook Abort Reasons › Build With --debug-prerender › focused client hook abort reasons › normal aborting › should capture useSelectedLayoutSegments before the data slot

expect(received).toMatchInlineSnapshot(snapshot)

Snapshot name: `Cache Components Errors - Client Hook Abort Reasons Build With --debug-prerender focused client hook abort reasons normal aborting should capture useSelectedLayoutSegments before the data slot 1`

- Snapshot  - 6
+ Received  + 6

@@ -27,17 +27,17 @@
  Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook
      at Object.useDynamicRouteParams (webpack:///<next-src>)
      at useSelectedLayoutSegments (webpack:///<next-src>)

  at UseSelectedLayoutSegments (../webpack:/app/client-hook-abort-reasons/client.tsx:37:28)
  at Page (../webpack:/app/client-hook-abort-reasons/normal/use-selected-layout-segments/[id]/page.tsx:7:7)
  -   647 |           // hang here and never resolve. This will cause the currently
  +   657 |           // hang here and never resolve. This will cause the currently
  -   648 |           // rendering component to effectively be a dynamic hole.
  +   658 |           // rendering component to effectively be a dynamic hole.
  - > 649 |           React.use(
  + > 659 |           React.use(
          |                 ^
  -   650 |             makeClientHookHangingPromise(
  +   660 |             makeClientHookHangingPromise(
  -   651 |               workUnitStore.renderSignal,
  +   661 |               workUnitStore.renderSignal,
  -   652 |               new ClientHookDynamicError(workStore.route, expression) {
  +   662 |               new ClientHookDynamicError(workStore.route, expression) {
      digest: 'CLIENT_HOOK_DYNAMIC'
    }
    To debug the issue, start the app in development mode by running `next dev`, then open "/client-hook-abort-reasons/normal/use-selected-layout-segments/[id]" in your browser to investigate the error.
    Error occurred prerendering page "/client-hook-abort-reasons/normal/use-selected-layout-segments/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
    ↵
  at Object.toMatchInlineSnapshot (e2e/app-dir/cache-components-errors/client-hook-abort-reasons.test.ts:1159:32)

● Cache Components Errors - Client Hook Abort Reasons › Build With --debug-prerender › focused client hook abort reasons › normal aborting › should capture useSelectedLayoutSegment after the data slot

expect(received).toMatchInlineSnapshot(snapshot)

Snapshot name: `Cache Components Errors - Client Hook Abort Reasons Build With --debug-prerender focused client hook abort reasons normal aborting should capture useSelectedLayoutSegment after the data slot 1`

- Snapshot  - 6
+ Received  + 6

@@ -27,17 +27,17 @@
  Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook
      at Object.useDynamicRouteParams (webpack:///<next-src>)
      at useSelectedLayoutSegment (webpack:///<next-src>)

  at UseSelectedLayoutSegment (../webpack:/app/client-hook-abort-reasons/client.tsx:32:27)
  at Page (../webpack:/app/client-hook-abort-reasons/normal/use-selected-layout-segment/[id]/page.tsx:8:7)
  -   647 |           // hang here and never resolve. This will cause the currently
  +   657 |           // hang here and never resolve. This will cause the currently
  -   648 |           // rendering component to effectively be a dynamic hole.
  +   658 |           // rendering component to effectively be a dynamic hole.
  - > 649 |           React.use(
  + > 659 |           React.use(
          |                 ^
  -   650 |             makeClientHookHangingPromise(
  +   660 |             makeClientHookHangingPromise(
  -   651 |               workUnitStore.renderSignal,
  +   661 |               workUnitStore.renderSignal,
  -   652 |               new ClientHookDynamicError(workStore.route, expression) {
  +   662 |               new ClientHookDynamicError(workStore.route, expression) {
      digest: 'CLIENT_HOOK_DYNAMIC'
    }
    To debug the issue, start the app in development mode by running `next dev`, then open "/client-hook-abort-reasons/normal/use-selected-layout-segment/[id]" in your browser to investigate the error.
    Error occurred prerendering page "/client-hook-abort-reasons/normal/use-selected-layout-segment/[id]". Read more: https://nextjs.org/docs/messages/prerender-error
    ↵
  at Object.toMatchInlineSnapshot (e2e/app-dir/cache-components-errors/client-hook-abort-reasons.test.ts:1449:32)

● Cache Components Errors - Client Hook Abort Reasons › Build With --debug-prerender › focused client hook abort reasons › sync IO aborting › should capture useParams before sync IO and data

expect(received).toMatchInlineSnapshot(snapshot)

Snapshot name: `Cache Components Errors - Client Hook Abort Reasons Build With --debug-prerender focused client hook abort reasons sync IO aborting should capture useParams before sync IO and data 1`

- Snapshot  - 6
+ Received  + 6

@@ -29,17 +29,17 @@
  Learn more: https://nextjs.org/docs/messages/blocking-prerender-client-hook
      at Object.useDynamicRouteParams (web

... truncated ...

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Stats from current PR

Warning

No stats were collected for Webpack because its stats job did not complete (it failed, was cancelled, or timed out). The results below only cover the bundlers that finished.

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 517 MB 518 MB 🔴 +1.21 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) 1.064s 1.065s █▁█▂▂
Cold (Ready in log) 1.041s 1.033s █▁█▁▂
Cold (First Request) 1.753s 1.750s █▁█▂▃
Warm (Listen) 1.063s 1.064s █▂█▂▂
Warm (Ready in log) 1.030s 1.040s █▁█▁▂
Warm (First Request) 821ms 803ms █▁█▂▂

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 7.247s 7.307s █▁█▂▄
Cached Build 7.261s 7.346s █▁█▃▃
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
node_modules Size 517 MB 518 MB 🔴 +1.21 MB (+0%) █▁▁██
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
03frh1tm1pkvs.js gzip 65.6 kB N/A -
0a-p0meqxynjz.js gzip 10.3 kB N/A -
0avtke4yefj4-.js gzip 13.1 kB N/A -
0ban--bx2qns6.js gzip 155 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0ke29q58efn1a.js gzip 156 B N/A -
0m3evdsbel0p0.js gzip 153 B N/A -
0p6ivy9lb7-ut.js gzip 157 B N/A -
0pt5of3ch6d2u.js gzip 13.6 kB N/A -
0sug3ihj23mvn.js gzip 154 B N/A -
0xnsdo-6y9r3k.js gzip 5.72 kB N/A -
0y25v1ej71uft.js gzip 155 B N/A -
0yi7is8u4covm.js gzip 8.77 kB N/A -
12zd-d7xicsb4.js gzip 158 B N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1g2ea05dkmmwi.js gzip 45.2 kB N/A -
1mqxdmso4160h.js gzip 160 B N/A -
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 -
2d91wcgx6lf2k.js gzip 153 B N/A -
2koflviradwt3.js gzip 7.4 kB N/A -
2lviwqiqxm904.js gzip 3.52 kB N/A -
2ocpv0tylryvn.js gzip 160 B N/A -
2pevdqjpbf3xk.js gzip 9.45 kB 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 -
2xsvwiy3-s105.js gzip 156 B N/A -
2zhsfsomo6g1v.js gzip 169 B 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 -
3qn7vntp30gaj.js gzip 1.47 kB N/A -
3s5fbzg2qit8e.js gzip 156 B N/A -
3xtfvhlgn22av.js gzip 71.1 kB N/A -
turbopack-0z.._-wk.js gzip 3.78 kB N/A -
turbopack-0z..g5ya.js gzip 3.8 kB N/A -
turbopack-10..hlo9.js gzip 3.8 kB N/A -
turbopack-11..yksz.js gzip 3.81 kB N/A -
turbopack-1k..5le3.js gzip 3.8 kB N/A -
turbopack-2p..8e3n.js gzip 3.8 kB N/A -
turbopack-2s..l7od.js gzip 3.8 kB N/A -
turbopack-2u..c9m0.js gzip 3.8 kB N/A -
turbopack-39..3ysu.js gzip 3.8 kB N/A -
turbopack-3i..u_md.js gzip 3.8 kB N/A -
turbopack-3l..z_a8.js gzip 3.81 kB N/A -
turbopack-3w..t926.js gzip 3.8 kB N/A -
turbopack-3y..v370.js gzip 3.82 kB N/A -
turbopack-40..h7ed.js gzip 3.8 kB N/A -
030a1njeh5b77.js gzip N/A 10 kB -
07fd-_9-0ojl-.js gzip N/A 45.6 kB -
0ceaj3rmii5dl.js gzip N/A 157 B -
0ot0qt5np20h7.js gzip N/A 8.78 kB -
0po7u01wwhl8s.js gzip N/A 157 B -
12m9rr7_7cexd.js gzip N/A 8.7 kB -
15-b7iytymgp5.js gzip N/A 65.6 kB -
186r3y_rfvr0b.js gzip N/A 8.78 kB -
19v97848yrjp7.js gzip N/A 8.81 kB -
1b9oo_n-opj3l.js gzip N/A 156 B -
1mvdo2etbyclo.js gzip N/A 156 B -
1nd_0rwgz2ozk.js gzip N/A 9.45 kB -
1oi9pltr_e10m.js gzip N/A 10.6 kB -
1qow5vb4es1bl.js gzip N/A 152 B -
1rd9tzqgzsz2w.js gzip N/A 8.77 kB -
1rewgj8du8r7b.js gzip N/A 157 B -
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 -
2-4bvun4utbsg.js gzip N/A 157 B -
200zi2gld9tg3.js gzip N/A 71.1 kB -
21h56wdn9zddo.js gzip N/A 8.75 kB -
21kc-4x8zf_oh.js gzip N/A 155 B -
22nt-vwpfkpq5.js gzip N/A 155 B -
2j9dbydew6g_b.js gzip N/A 162 B -
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 -
2zvoj8k1z3cei.js gzip N/A 7.41 kB -
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 -
37pm_isc9xj9i.js gzip N/A 163 B -
3vdh6_u50pu5t.js gzip N/A 13.6 kB -
3zbnmofunn_bw.js gzip N/A 158 B -
45148p5lps61x.js gzip N/A 170 B -
turbopack-00..m1m7.js gzip N/A 3.81 kB -
turbopack-0d..4kzx.js gzip N/A 3.81 kB -
turbopack-0z..p79s.js gzip N/A 3.81 kB -
turbopack-1c..77uv.js gzip N/A 3.81 kB -
turbopack-1i..3kn6.js gzip N/A 3.81 kB -
turbopack-1t..w2r4.js gzip N/A 3.79 kB -
turbopack-2d..v5q3.js gzip N/A 3.82 kB -
turbopack-2k..d454.js gzip N/A 3.81 kB -
turbopack-2t..j9f5.js gzip N/A 3.81 kB -
turbopack-3_..5w5s.js gzip N/A 3.81 kB -
turbopack-39..bm8d.js gzip N/A 3.81 kB -
turbopack-3e..0dhh.js gzip N/A 3.8 kB -
turbopack-3t..czon.js gzip N/A 3.81 kB -
turbopack-3v..bfyi.js gzip N/A 3.81 kB -
Total 448 kB 448 kB ⚠️ +527 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 778 B 776 B
Total 778 B 776 B ✅ -2 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 436 B 435 B
Total 436 B 435 B ✅ -1 B

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 366 kB 366 kB
app-page-exp..prod.js gzip 202 kB 202 kB
app-page-tur...dev.js gzip 365 kB 366 kB
app-page-tur..prod.js gzip 202 kB 202 kB
app-page-tur...dev.js gzip 362 kB 362 kB
app-page-tur..prod.js gzip 200 kB 200 kB
app-page.run...dev.js gzip 362 kB 363 kB
app-page.run..prod.js gzip 200 kB 200 kB
app-route-ex...dev.js gzip 81.7 kB 81.9 kB
app-route-ex..prod.js gzip 55.6 kB 55.6 kB
app-route-tu...dev.js gzip 81.8 kB 81.9 kB
app-route-tu..prod.js gzip 55.6 kB 55.7 kB
app-route-tu...dev.js gzip 81.4 kB 81.5 kB
app-route-tu..prod.js gzip 55.4 kB 55.4 kB
app-route.ru...dev.js gzip 81.3 kB 81.5 kB
app-route.ru..prod.js gzip 55.3 kB 55.4 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.7 kB 39.7 kB
pages.runtim...dev.js gzip 54.8 kB 54.8 kB
pages.runtim..prod.js gzip 39.7 kB 39.7 kB
server.runti..prod.js gzip 67.8 kB 67.8 kB
use-cache-pr...dev.js gzip 71.6 kB 71.6 kB
use-cache-pr...dev.js gzip 71.6 kB 71.6 kB
use-cache-pr...dev.js gzip 69.9 kB 69.9 kB
use-cache-pr...dev.js gzip 69.9 kB 69.9 kB
Total 3.51 MB 3.51 MB ⚠️ +4.55 kB
📝 Changed Files (18 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
  • app-route-ex..ntime.dev.js
  • app-route-ex..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route-tu..ntime.dev.js
  • app-route-tu..time.prod.js
  • app-route.runtime.dev.js
  • app-route.ru..time.prod.js
  • pages-api.ru..time.prod.js
  • pages.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js
failed to diff
app-route-ex..ntime.dev.js

Diff too large to display

app-route-ex..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route-tu..ntime.dev.js

Diff too large to display

app-route-tu..time.prod.js

Diff too large to display

app-route.runtime.dev.js

Diff too large to display

app-route.ru..time.prod.js

Diff too large to display

pages-api.ru..time.prod.js

Diff too large to display

pages.runtime.prod.js

Diff too large to display

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

Commit: c53c7fa

This is a refactor of the prefetching algorithm to block the prefetch task
from completing until the response is received.

Currently, a prefetch task is fire-and-forget: once a prefetch's requests are
spawned, the scheduler marks the originating task as complete regardless of
what the server ends up returning. However, certain planned optimizations
require us to verify the response from the server to determine whether
additional work is necessary. For example, we may attempt to do a static
prefetch instead of a runtime prefetch if we optimistically assume that the
static prefetch does not vary on runtime data. We need to verify that
assumption is correct before marking the task as complete.

Now, whenever a pass over the task spawns a segment request, or encounters an
entry whose response hasn't arrived yet, the task registers itself on the
entry and exits as Blocked instead of Done. When the entry resolves, the
blocked task is pinged and the pass re-runs against the received data. Only a
pass that observes every response it cares about advances the task to the
next phase or completes it.

Rejected entries are handled per segment: a rejection counts as an observed
response, so the pass skips that segment and keeps prefetching the rest. The
task never registers on a rejected entry, since nothing ever pings one.
Retries are governed by the existing per-entry logic.

Also fixes a cache bug this exposed: when a revalidation response is keyed at
a more generic vary path than the stale partial entry that prompted it
(because the segment turned out not to vary on some param), the stale entry
shadowed the result on every lookup — the upgraded data was unreachable and
the revalidation was wasted. With blocking, that dead end became an infinite
loop: each fulfillment re-pinged the blocked task, which re-read the same
stale entry and spawned another revalidation. Upserting (or re-keying) a
segment now deletes settled entries at more specific vary paths that the
incoming entry supersedes.

The router-act test helper's "at least one request initiated" watchdog is
adjusted to account for the new blocking behavior: countable requests can now
be legitimately gated on an in-flight App Shell response (which the watchdog
intentionally doesn't count). On expiry it now proceeds to response
processing if the act scope intercepted a shell request, and keeps waiting
while shell requests are in flight elsewhere on the page, instead of
timing out.
@acdlite
acdlite force-pushed the segment-prefetch-needs-runtime-request branch from 5935469 to 81202cf Compare July 21, 2026 18:03
@acdlite
acdlite force-pushed the segment-prefetch-block-until-response branch from f1ad2b9 to c53c7fa Compare July 21, 2026 18:03
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.

1 participant