cacheComponents bfcache: <Activity> children are ordered most-recently-active, so returning to a route re-parents its DOM and reloads every iframe inside it (2nd visit onward)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/morethanmin/next-bfcache-iframe-repro
To Reproduce
The linked reproduction is a Next app with cacheComponents: true, two static routes, and one <iframe> on /. The iframe's document prints the time it loaded.
npm install
npm run build
npm start # http://localhost:3055
- Open
/. Note the time printed inside the iframe. - Click go to /b, then back to /. The printed time is unchanged — the iframe was preserved, which is the point of the bfcache.
- Do the same round trip a second time. The printed time changes — the iframe's document was discarded and re-fetched.
- Every further round trip reloads it again. Browser back/forward behaves the same way.
probe.mjs in the repo counts Page.frameNavigated for the iframe URL:
| round trip | iframe reloads (cumulative) |
|---|---|
| 1 | 0 |
| 2 | 1 |
| 3 | 2 |
Same numbers in Chromium 151 and WebKit 26.5, so this is not a single-engine quirk.
Current vs. Expected behavior
Current: from the second visit onward, returning to a preserved route re-parents that route's DOM subtree, so any <iframe> inside it is torn down and reloaded. In our app a YouTube embed rewinds to its start every other time the user comes back to the home route; the same would hit any third-party iframe that holds state — payments, maps, editors, chat widgets.
The ordering of the bfcache list looks like the cause. useRouterBFCache returns its entries most recently active first, and LayoutRouter pushes them into children in that order:
// packages/next/src/client/components/layout-router.tsx
do {
// ...
children.push(child) // <Activity key={stateKey}>
bfcacheEntry = bfcacheEntry.next
} while (bfcacheEntry !== null)
So the keyed sibling order flips on every navigation: [home] → [b, home] → [home, b] → [b, home]. React reconciles the reorder with a DOM move (insertBefore on a connected node), and per spec that discards an iframe's nested browsing context. The first round trip happens to move the other route's subtree, which is why the bug only appears from the second one.
CDP on the reload:
Page.frameRequestedNavigation reason=initialFrameNavigation url=<iframe src>
Network.requestWillBeSent type=Document initiator=script (React DOM placement/commit frames)
Expected: a route preserved in the bfcache keeps its DOM subtree in place, so iframes inside it are not reloaded. Hidden <Activity> children are display: none, so their sibling order has no visual effect and does not need to track recency.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.5.0
Available memory (MB): 32768
Available CPU cores: 12
Binaries:
Node: 24.12.0
npm: 11.6.2
Yarn: 4.14.1
pnpm: N/A
Relevant Packages:
next: 16.4.0-canary.26
eslint-config-next: N/A
react: 19.2.0
react-dom: 19.2.0
typescript: N/A
Next.js Config:
output: N/A
The reproduction is pinned to 16.4.0-canary.26. Also reproduces on stable 16.2.10.
Which area(s) are affected? (Select all that apply)
cacheComponents, Linking and Navigating
Which stage(s) are affected? (Select all that apply)
next build (local), next start (local)
Additional context
Sorting children by key before returning them makes the sibling order stable and removes the reload in both engines (3 round trips, 0 iframe reloads). We are running this as a local patch:
// packages/next/src/client/components/layout-router.tsx — after the do/while
if (children.length > 1) {
children.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
}
That is only meant to show that ordering is the cause. The TODO already in bfcache-state-manager.ts (about using the history order instead) would fix it too and is the more principled ordering.
Element.prototype.moveBefore() would preserve iframe state across a move, but react-dom does not use it (19.3.0, canary and experimental builds contain no reference to it) and Safari 26.5 does not implement it, so it is not a workaround available today.
Related: #86577 tracks other <Activity> route-preservation breakages, but does not cover DOM re-parenting or iframes.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Run the linked reproduction with npm install, npm run build, and npm start, then use probe.mjs to confirm iframe reloads after repeated round trips. Read packages/next/src/client/components/layout-router.tsx and the TODO in bfcache-state-manager.ts to understand the bfcache entry ordering. Done means preserved route subtrees keep stable sibling placement and the reproduction reports zero iframe reloads across repeated navigations.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, react
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100