Skip to content

Handle a purged browser cache on back navigation#94317

Merged
unstubbable merged 1 commit into
hl/fix-eslint-default-casefrom
hl/back-nav-edge-case
Jun 2, 2026
Merged

Handle a purged browser cache on back navigation#94317
unstubbable merged 1 commit into
hl/fix-eslint-default-casefrom
hl/back-nav-edge-case

Conversation

@unstubbable
Copy link
Copy Markdown
Contributor

@unstubbable unstubbable commented Jun 1, 2026

When the browser's HTTP cache entry for a back-navigation target has been evicted between forward visit and back-press — for example with a long-lived tab, storage pressure, or a manual cache clear — the browser re-fetches the document fresh from the server. Previously we relied on type === 'back_forward' alone to decide that the document came from cache, which meant we treated those re-fetches as cache restores too. The persisted-chunk lookup would miss on the fresh response, and we'd recover with an unnecessary location.reload().

The same edge case is exposed by the Playwright/WebKit combination in the upstack PR: bfcache isn't utilized there, so back-navigations always come over the network, and the old single-signal check classified those re-fetches as served-from-cache and triggered the same unnecessary reload.

With this change we split the cache-restore detection into two phases. At script-execution time we look at deliveryType (Chrome ≥109, Safari ≥17) and the navigation entry's transferSize/encodedBodySize to decide cache-restore vs fresh-response when those fields are populated. When the body bytes haven't been measured yet — the common case for streaming Next.js dev RSC responses on every browser, and also WebKit leaving both size fields at zero at exec time — we suspend the readable until pageshow and re-check there. On a fresh re-fetch we route through the live WebSocket-backed channel, which already has the debug data for the new response, and skip the reload entirely.

The new bfcache-regression test exercises this edge case on Chromium by clearing the browser cache via CDP between the forward and back navigations, using a new clearBrowserCache() helper on the test browser wrapper. The same exec-time code path is naturally hit by Safari whenever its navigation entry's size fields are still zero at script-execution time, but the harness can't force the eviction deterministically there.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Tests Passed

Commit: 2762a4e

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Stats from current PR

🔴 1 regression

Metric Canary PR Change Trend
node_modules Size 508 MB 508 MB 🔴 +104 kB (+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) 812ms 810ms ▁▃▆█▆
Cold (Ready in log) 785ms 788ms ▁▃▂█▃
Cold (First Request) 1.205s 1.212s ▂▄▄█▅
Warm (Listen) 810ms 811ms ▂▄▄▇▅
Warm (Ready in log) 786ms 786ms ▁▅▄█▂
Warm (First Request) 603ms 607ms ▂▆▅█▂
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 811ms 810ms █████
Cold (Ready in log) 789ms 789ms ▇██▆▆
Cold (First Request) 3.207s 3.191s ▅▅▇▄▂
Warm (Listen) 809ms 810ms █████
Warm (Ready in log) 788ms 791ms ▇█▇▆▆
Warm (First Request) 3.228s 3.215s ▅▆▆▃▁

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.088s 5.183s ▂▆▃▃▄
Cached Build 5.164s 5.145s ▃▄▅▇▄
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 23.913s 24.269s ▁▇█▄▃
Cached Build 23.979s 24.065s ▁▆█▁▁
node_modules Size 508 MB 508 MB 🔴 +104 kB (+0%) ▁▁███
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
04hm05ar7kldw.js gzip 5.73 kB N/A -
08eoeyxjgkcg-.js gzip 71 kB N/A -
08wo8crzyq6vd.js gzip 159 B N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0dvitrl5zg37g.js gzip 8.82 kB N/A -
0sf7ysou-72zd.js gzip 8.71 kB N/A -
10-wxe5rnc8w4.js gzip 156 B N/A -
157abun3hwc_s.js gzip 10.3 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1fir9z67zbzhj.js gzip 156 B N/A -
1jpaub6y8xlfr.js gzip 2.3 kB N/A -
1ot0mvscrc_uf.js gzip 233 B N/A -
1pwdlnd2apq1-.js gzip 169 B N/A -
1vd-btw8knlq8.js gzip 156 B N/A -
2_m3xv2uq3sjc.js gzip 1.46 kB N/A -
20s37uade9if-.js gzip 154 B N/A -
21gk5ltqgd84s.js gzip 65.6 kB N/A -
24y34mwgrkqp4.js gzip 8.78 kB N/A -
28ty09pswlre7.js gzip 155 B N/A -
2ajcnrb7u4z-2.js gzip 156 B N/A -
2c-fd4y1zozz8.js gzip 8.79 kB N/A -
2d7416h_xd36x.js gzip 8.71 kB N/A -
2lyuhit6rn8fy.js gzip 9.44 kB N/A -
2q0gr8wfr3jwl.js gzip 8.77 kB N/A -
2rr4cc2-omm4l.js gzip 151 B N/A -
2t9e75oz6r0zp.js gzip 8.76 kB N/A -
2tu4ozix-297g.js gzip 7.61 kB N/A -
2uku_olcn15b7.js gzip 8.79 kB N/A -
2wdfehtb6jwpt.js gzip 159 B N/A -
2x075h1zduvno.js gzip 156 B N/A -
30r8mm-46bdqy.js gzip 220 B 220 B
340x4rsgf3u0g.js gzip 13.8 kB N/A -
3a3ds3ahws_02.js gzip 155 B N/A -
3c1jdxkzlb8oq.js gzip 12.9 kB N/A -
3inab2jybr4k9.js gzip 450 B N/A -
3jkm5tdjvaf_q.js gzip 13.1 kB N/A -
3lkaoispiaba2.js gzip 154 B N/A -
3mt67agm5wp40.js gzip 10.6 kB N/A -
3npr-wfnuzppc.js gzip 50.5 kB N/A -
3saabek4kohwi.js gzip 10 kB N/A -
4189xmby9yu1p.js gzip 13.6 kB N/A -
turbopack-03..xgxj.js gzip 4.23 kB N/A -
turbopack-0j..ahzr.js gzip 4.21 kB N/A -
turbopack-0l..b7dv.js gzip 4.23 kB N/A -
turbopack-0m..9wuj.js gzip 4.23 kB N/A -
turbopack-0n..o209.js gzip 4.24 kB N/A -
turbopack-1w..493-.js gzip 4.23 kB N/A -
turbopack-24..ebz0.js gzip 4.23 kB N/A -
turbopack-25..qcev.js gzip 4.23 kB N/A -
turbopack-2d..x4fs.js gzip 4.23 kB N/A -
turbopack-2m..w4cs.js gzip 4.23 kB N/A -
turbopack-2o..0_jd.js gzip 4.23 kB N/A -
turbopack-2t..kx-7.js gzip 4.23 kB N/A -
turbopack-2v..f6e0.js gzip 4.23 kB N/A -
turbopack-41..kszv.js gzip 4.23 kB N/A -
0_i7nqgx23st7.js gzip N/A 10 kB -
0_ue9iqdenfn5.js gzip N/A 169 B -
0-u-mm1c6xdx5.js gzip N/A 50.5 kB -
06puhytyxk31p.js gzip N/A 8.82 kB -
0a_4hmdwd-3ry.js gzip N/A 153 B -
0bu428s3duhqp.js gzip N/A 7.61 kB -
0j42f9zonj0wd.js gzip N/A 13 kB -
0jpt_hm78rtws.js gzip N/A 155 B -
0m34gln_kt4fg.js gzip N/A 5.73 kB -
0n2gf9b36q_in.js gzip N/A 152 B -
144ni8t6ydmli.js gzip N/A 156 B -
19b4xchn6cihb.js gzip N/A 157 B -
1g3q1ww01thnl.js gzip N/A 2.3 kB -
1hraqxuiymq6v.js gzip N/A 8.79 kB -
1l9un1sl77287.js gzip N/A 1.46 kB -
1r7l_zy2yrasy.js gzip N/A 154 B -
2147zgtf14z-q.js gzip N/A 234 B -
23bz3xsg-5-1s.js gzip N/A 8.71 kB -
24wnwxg8azxgr.js gzip N/A 156 B -
27441mytv7pbm.js gzip N/A 9.43 kB -
2cjkwjgm1zcfs.js gzip N/A 8.71 kB -
2hl4t97p3y84-.js gzip N/A 156 B -
2mwnolbec-x0c.js gzip N/A 71 kB -
2scd8zaoyb8md.js gzip N/A 8.79 kB -
2st_qs6p_9us0.js gzip N/A 13.1 kB -
2yq3gaznuimnq.js gzip N/A 156 B -
2zdlhgg2czq_1.js gzip N/A 65.6 kB -
2zo2exm1d8qj1.js gzip N/A 13.6 kB -
30oszfu8bgqbc.js gzip N/A 13.9 kB -
33414okov1b1o.js gzip N/A 162 B -
34-oyj6z4526_.js gzip N/A 160 B -
3hn75zuxly9az.js gzip N/A 10.3 kB -
3hqh7m128tvsn.js gzip N/A 8.77 kB -
3hqti_t-zy1x4.js gzip N/A 449 B -
3mnawenie1flm.js gzip N/A 8.76 kB -
3ubsozlu6zs38.js gzip N/A 10.6 kB -
43iwfqjnx1cy_.js gzip N/A 8.78 kB -
44juzboy2rnti.js gzip N/A 156 B -
turbopack-0b..8cuh.js gzip N/A 4.21 kB -
turbopack-0b..hy8d.js gzip N/A 4.22 kB -
turbopack-0o..qwq4.js gzip N/A 4.23 kB -
turbopack-0r..9y5r.js gzip N/A 4.24 kB -
turbopack-1i..cyhh.js gzip N/A 4.23 kB -
turbopack-1l..wovj.js gzip N/A 4.23 kB -
turbopack-1m..ozz4.js gzip N/A 4.23 kB -
turbopack-2-..vqc6.js gzip N/A 4.23 kB -
turbopack-25..a8eb.js gzip N/A 4.23 kB -
turbopack-2e..4jps.js gzip N/A 4.23 kB -
turbopack-2l..r-5s.js gzip N/A 4.23 kB -
turbopack-2r..ps2o.js gzip N/A 4.23 kB -
turbopack-2y..3akc.js gzip N/A 4.23 kB -
turbopack-45..yr-0.js gzip N/A 4.22 kB -
Total 470 kB 470 kB ⚠️ +39 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 724 B 725 B
Total 724 B 725 B ⚠️ +1 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 433 B 433 B
Total 433 B 433 B

📦 Webpack

Client

Main Bundles
Canary PR Change
2258-HASH.js gzip 61.7 kB N/A -
2266-HASH.js gzip 4.69 kB N/A -
3317.HASH.js gzip 169 B N/A -
4866-HASH.js gzip 5.64 kB N/A -
9e302639-HASH.js gzip 62.8 kB N/A -
framework-HASH.js gzip 59.5 kB 59.5 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.9 kB 39.9 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
175fd0fd-HASH.js gzip N/A 62.8 kB -
2596-HASH.js gzip N/A 5.63 kB -
34-HASH.js gzip N/A 61.7 kB -
5691.HASH.js gzip N/A 169 B -
9156-HASH.js gzip N/A 4.68 kB -
Total 236 kB 236 kB ✅ -17 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 193 B 193 B
_error-HASH.js gzip 181 B 182 B
css-HASH.js gzip 334 B 332 B
dynamic-HASH.js gzip 1.79 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 255 B
head-HASH.js gzip 351 B 348 B
hooks-HASH.js gzip 385 B 384 B
image-HASH.js gzip 580 B 580 B
index-HASH.js gzip 257 B 259 B
link-HASH.js gzip 2.51 kB 2.52 kB
routerDirect..HASH.js gzip 318 B 319 B
script-HASH.js gzip 387 B 386 B
withRouter-HASH.js gzip 316 B 316 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.99 kB ⚠️ +19 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 126 kB 126 kB
page.js gzip 277 kB 272 kB 🟢 5.26 kB (-2%)
Total 404 kB 398 kB ✅ -5.47 kB
Middleware
Canary PR Change
middleware-b..fest.js gzip 618 B 615 B
middleware-r..fest.js gzip 155 B 155 B
middleware.js gzip 44.3 kB 44.7 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 46 kB 46.3 kB ⚠️ +345 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 719 B 717 B
Total 719 B 717 B ✅ -2 B
Build Cache
Canary PR Change
0.pack gzip 4.51 MB 4.5 MB 🟢 5.52 kB (0%)
index.pack gzip 115 kB 116 kB
index.pack.old gzip 115 kB 113 kB
Total 4.74 MB 4.73 MB ✅ -6.35 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 352 kB 352 kB
app-page-exp..prod.js gzip 196 kB 196 kB
app-page-tur...dev.js gzip 352 kB 352 kB
app-page-tur..prod.js gzip 196 kB 196 kB
app-page-tur...dev.js gzip 348 kB 348 kB
app-page-tur..prod.js gzip 194 kB 194 kB
app-page.run...dev.js gzip 348 kB 349 kB
app-page.run..prod.js gzip 194 kB 194 kB
app-route-ex...dev.js gzip 77.5 kB 77.5 kB
app-route-ex..prod.js gzip 52.9 kB 52.9 kB
app-route-tu...dev.js gzip 77.6 kB 77.6 kB
app-route-tu..prod.js gzip 52.9 kB 52.9 kB
app-route-tu...dev.js gzip 77.2 kB 77.2 kB
app-route-tu..prod.js gzip 52.7 kB 52.7 kB
app-route.ru...dev.js gzip 77.1 kB 77.1 kB
app-route.ru..prod.js gzip 52.7 kB 52.7 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 44.3 kB 44.3 kB
pages-api-tu..prod.js gzip 33.8 kB 33.8 kB
pages-api.ru...dev.js gzip 44.3 kB 44.3 kB
pages-api.ru..prod.js gzip 33.7 kB 33.7 kB
pages-turbo....dev.js gzip 53.7 kB 53.7 kB
pages-turbo...prod.js gzip 39.4 kB 39.4 kB
pages.runtim...dev.js gzip 53.6 kB 53.6 kB
pages.runtim..prod.js gzip 39.4 kB 39.4 kB
server.runti..prod.js gzip 63.2 kB 63.2 kB
use-cache-pr...dev.js gzip 70 kB 70 kB
use-cache-pr...dev.js gzip 70 kB 70 kB
use-cache-pr...dev.js gzip 68.4 kB 68.4 kB
use-cache-pr...dev.js gzip 68.3 kB 68.3 kB
Total 3.38 MB 3.38 MB ⚠️ +1.08 kB
📝 Changed Files (4 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..ntime.dev.js
  • app-page.runtime.dev.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page-tur..ntime.dev.js
failed to diff
app-page.runtime.dev.js
failed to diff
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/2762a4e25cbabdde068431fd9aee38df24b19690/next

Commit: 2762a4e

@unstubbable unstubbable marked this pull request as ready for review June 1, 2026 20:30
@unstubbable unstubbable requested a review from lubieowoce June 1, 2026 20:30
@unstubbable unstubbable force-pushed the hl/fix-eslint-default-case branch from a43a425 to a568bca Compare June 2, 2026 10:11
@unstubbable unstubbable force-pushed the hl/back-nav-edge-case branch from efcc5dd to 532801d Compare June 2, 2026 10:11
Comment thread eslint.config.mjs Outdated
When the browser's HTTP cache entry for a back-navigation target has
been evicted between forward visit and back-press — for example with a
long-lived tab, storage pressure, or a manual cache clear — the browser
re-fetches the document fresh from the server. Previously we relied on
`type === 'back_forward'` alone to decide that the document came from
cache, which meant we treated those re-fetches as cache restores too.
The persisted-chunk lookup would miss on the fresh response, and we'd
recover with an unnecessary `location.reload()`.

The same edge case is exposed by the Playwright/WebKit combination in
the upstack PR: bfcache isn't utilized there, so back-navigations always
come over the network, and the old single-signal check classified those
re-fetches as served-from-cache and triggered the same unnecessary
reload.

With this change we split the cache-restore detection into two phases.
At script-execution time we look at `deliveryType` (Chrome ≥109, Safari
≥17) and the navigation entry's `transferSize`/`encodedBodySize` to
decide cache-restore vs fresh-response when those fields are populated.
When the body bytes haven't been measured yet — the common case for
streaming Next.js dev RSC responses on every browser, and also WebKit
leaving both size fields at zero at exec time — we suspend the readable
until `pageshow` and re-check there. On a fresh re-fetch we route
through the live WebSocket-backed channel, which already has the debug
data for the new response, and skip the reload entirely.

The new `bfcache-regression` test exercises this edge case on Chromium
by clearing the browser cache via CDP between the forward and back
navigations, using a new `clearBrowserCache()` helper on the test
browser wrapper. The same exec-time code path is naturally hit by Safari
whenever its navigation entry's size fields are still zero at
script-execution time, but the harness can't force the eviction
deterministically there.
@unstubbable unstubbable force-pushed the hl/back-nav-edge-case branch from 532801d to 2762a4e Compare June 2, 2026 10:19
@unstubbable unstubbable merged commit abbfa7e into hl/fix-eslint-default-case Jun 2, 2026
292 of 298 checks passed
@unstubbable unstubbable deleted the hl/back-nav-edge-case branch June 2, 2026 12:46
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