mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#742
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/647
Author: @southpolesteve
Created: 3/22/2026
Status: 🔄 Open
Base:
main← Head:fix/suspense-flash-navigation-639📝 Commits (3)
1695009fix: prevent Suspense fallback flash during App Router client navigation (#639)f0d522afix: also fix back-button scroll restoration jank (#639)e952f80fix: buffer full RSC response before rendering to eliminate partial commits📊 Changes
8 files changed (+408 additions, -14 deletions)
View changed files
📝
packages/vinext/src/server/app-browser-entry.ts(+121 -7)📝
packages/vinext/src/shims/navigation.ts(+25 -7)📝
tests/e2e/app-router/loading.spec.ts(+38 -0)📝
tests/e2e/app-router/navigation-flows.spec.ts(+132 -0)📝
tests/fixtures/app-basic/app/page.tsx(+3 -0)➕
tests/fixtures/app-basic/app/suspense-nav-test/FilterToggle.tsx(+16 -0)➕
tests/fixtures/app-basic/app/suspense-nav-test/item/[id]/page.tsx(+28 -0)➕
tests/fixtures/app-basic/app/suspense-nav-test/page.tsx(+45 -0)📄 Description
Summary
flushSync(() => root.render(...))with aNavigationRootwrapper component that holds RSC content in React statestartTransition(() => setState(newContent))insideNavigationRoot, which tells React to keep the current committed UI visible until all Suspense boundaries in the new tree resolve, then commit atomicallyNavigationRootviaroot.render()and destroying the wrapperRoot cause
flushSyncforced a synchronous commit of the incoming RSC tree including any unresolved Suspense fallbacks. This produced a visible double-flash: content outside a Suspense boundary (e.g. a heading) updated immediately while content inside it still showed the loading fallback, before finally resolving.Why
startTransition(() => root.render(...))wasn't enoughroot.render()replaces the entire fiber tree, so React has no previously committed content to hold onto. New Suspense boundaries in the replacement tree can still flash their fallbacks.startTransition(() => setState(...))inside a persistent component is the correct approach: React keeps that component's current committed output visible until the new render (including all Suspense boundaries) is fully ready, matching Next.js App Router behavior.Changes
packages/vinext/src/server/app-browser-entry.ts—NavigationRootcomponent +_scheduleRscUpdatewiringtests/fixtures/app-basic/app/suspense-nav-test/— new fixture reproducing the exact partial-flash scenario from the issuetests/e2e/app-router/navigation-flows.spec.ts— regression tests for issue #639tests/e2e/app-router/loading.spec.ts— updated: removed incorrect assertion (showingloading.tsxduring navigation to a new route is correct Next.js behavior)Closes #639
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.