-
Notifications
You must be signed in to change notification settings - Fork 30k
fix: prevent duplicate RSC requests when navigation happens during inflight prefetch #86700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
fix: prevent duplicate RSC requests when navigation happens during inflight prefetch #86700
Conversation
… adding new ones in `router-server.ts`.
…and remove redundant TypeScript error comments.
…and remove redundant TypeScript error comments.
…riable name conflicts - Remove condition that only applied hygiene when mangle is disabled - Always apply hygiene transformation after minification to resolve variable name conflicts that occur during function inlining - Add snapshot test to verify the fix works correctly Fixes vercel#86568
- Add cascade fallback logic to AppRouterAnnouncer - Announce route changes when title, H1, or path changes - Prioritize title over H1, and H1 over path - Add comprehensive unit tests for all cascade scenarios Fixes vercel#86660
…idation Fix issue where Next.js was unable to determine the reason for an empty static shell when generateMetadata uses runtime data (e.g., connection()). Previously, when prelude === PreludeState.Empty and generateMetadata accessed runtime data, Next.js would throw a generic error: 'Route did not produce a static shell and Next.js was unable to determine a reason. This is a bug in Next.js.' Now, the code checks for dynamicMetadata before throwing the generic error, providing a specific error message about runtime data being accessed in generateMetadata. Changes: - Added dynamicMetadata check in getStaticShellDisallowedDynamicReasons when prelude === PreludeState.Empty - Added dynamicMetadata check in throwIfDisallowedDynamic when prelude === PreludeState.Empty - Added test case to verify the fix Fixes vercel#86664
- Normalize loaderFile path in create-compiler-aliases.ts to convert backslashes to forward slashes on Windows - Fix absolute path handling in config.ts to prevent path duplication - Add integration test for Windows-specific loaderFile behavior Fixes vercel#86614
…nabled Intercepting routes depend on the Next-URL header which may not be available during prerender, causing cacheComponents errors. This fix adds a check to detect intercepting routes and skip prerender for them when cacheComponents is enabled. Changes: - Add pathCouldBeIntercepted() method to check if a route can be intercepted - Check for intercepting routes before starting prerender with cacheComponents - Make loadManifests() protected in RouteModule to allow access to interceptionRoutePatterns - Skip prerender and mark route as dynamic when intercepting route is detected Fixes vercel#86444
- Add isESMProject() method to check if package.json has "type": "module" - Update loadNodeMiddleware() to use dynamic import() instead of require() for ESM projects - Handle Windows paths with pathToFileURL() for ESM imports - Add test suite for ESM-only projects with proxy middleware Fixes vercel#86434
- Add automatic includePaths configuration for Sass in Turbopack to ensure node_modules directory is included for SCSS import resolution - Normalize resource paths before passing to sass-loader on Windows to handle backslash path separators correctly - Add test to verify Bootstrap SCSS imports work with Turbopack Fixes vercel#86431
…ring build Fix build crash when Route Handlers use notFound() with Turbopack. The issue was that shared-modules.ts tried to import app-router-context which is not available for Route Handlers during Turbopack build. This fix uses try-catch to handle the case when the module is not available and exports an empty namespace instead, since Route Handlers don't actually need app-router-context. Adds test to verify Route Handlers with notFound() build successfully with Turbopack. Fixes vercel#86390
When cacheComponents is enabled, component scripts were using async: true which can block the main thread during execution. This change replaces async with defer to ensure scripts execute after HTML parsing, reducing Total Blocking Time (TBT) as reported in issue vercel#86383. - Replace async: true with defer: true in createComponentStylesAndScripts - Replace async: true with defer: true in getLayerAssets - Add test to verify defer attribute is used for component scripts
Fix issue where updating search params on a page that could be intercepted would incorrectly trigger the interception route. This happens because nextUrl was being passed even when only search params changed (pathname unchanged). Changes: - Add check in navigateReducer to compare pathname before passing nextUrl - If pathname hasn't changed, pass null instead of state.nextUrl to prevent interception routes from being triggered during search param updates - Add test case to verify the fix works correctly Fixes vercel#86362
Fix type annotation error by using RcStr::from() instead of to_string().await? This resolves the issue where ReadRef<RcStr> cannot be moved out of dereference.
- Replace async path conversion with `RcStr::from` for efficiency - Ensure correct string conversion when adding node_modules to includePaths Fixes potential issues with SCSS imports resolution
…using --webpack Remove separate isProxyFile handling in runDependingOnPageType to allow proxy files to be handled through isMiddlewareFile, which correctly routes them to edge server entrypoints. Previously, proxy files were routed only to onServer() instead of onEdgeServer(), causing them to not be compiled as edge middleware in production builds with webpack when using custom pageExtensions. Fixes vercel#86303
Add handling for visibilitychange and pagehide events to finalize LCP metric when page becomes hidden or is being unloaded. This ensures LCP is reported even when users are idle and don't interact with the page. The fix: - Tracks LCP entries using Performance API - Finalizes LCP when visibilityState becomes 'hidden' or on pagehide - Prevents duplicate reporting by tracking if LCP was already reported - Uses renderTime/loadTime from LCP entry for accurate metric value Fixes vercel#86291
Fix RSC error when using cookies() in layout.tsx with basePath configured. The issue was that basePath was not being removed from parsedUrl.pathname after normalizing RSC requests, causing cookies() to fail finding the request context. - Remove basePath from pathname after RSC normalization in handleRSCRequest - Add test case to verify cookies() works correctly in layout with basePath Fixes vercel#86284
…flight prefetch When a prefetch is already in progress (Pending status), navigation was immediately making a new RSC request instead of waiting for the prefetch to complete. This caused duplicate requests when users clicked links quickly after prefetch started. This fix adds: - Promise field to PendingRouteCacheEntry to track waiting navigation - waitForRouteCacheEntry function to wait for pending prefetch completion - Navigation logic to wait for inflight prefetch before making new request - Test to verify only one request is made when prefetch is inflight Fixes vercel#86400
|
Notifying the following users due to files changed in this PR based on this repo's notify modifiers: @timneutkens, @ijjk, @shuding, @huozhi: |
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
| if (promiseToResolve !== null) { | ||
| promiseToResolve.resolve(fulfilledEntry) | ||
| // Free the promise for garbage collection. | ||
| if (entry.status === EntryStatus.Pending) { | ||
| const pendingEntry = entry as PendingRouteCacheEntry | ||
| pendingEntry.promise = null | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Promise cleanup logic will never execute due to status check happening after mutation. After fulfilledEntry.status = EntryStatus.Fulfilled is set on line 932, the condition if (entry.status === EntryStatus.Pending) on line 944 will always be false since entry and fulfilledEntry reference the same object. This means pendingEntry.promise = null on line 946 never executes, causing a memory leak.
Fix by moving the cleanup before the status check or removing the redundant check:
if (promiseToResolve !== null) {
promiseToResolve.resolve(fulfilledEntry)
// Free the promise for garbage collection.
const pendingEntry = entry as PendingRouteCacheEntry
pendingEntry.promise = null
}| if (promiseToResolve !== null) { | |
| promiseToResolve.resolve(fulfilledEntry) | |
| // Free the promise for garbage collection. | |
| if (entry.status === EntryStatus.Pending) { | |
| const pendingEntry = entry as PendingRouteCacheEntry | |
| pendingEntry.promise = null | |
| } | |
| } | |
| if (promiseToResolve !== null) { | |
| promiseToResolve.resolve(fulfilledEntry) | |
| // Free the promise for garbage collection. | |
| const pendingEntry = entry as PendingRouteCacheEntry | |
| pendingEntry.promise = null | |
| } |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
| if (entry.status === EntryStatus.Pending) { | ||
| const pendingEntry = entry as PendingRouteCacheEntry | ||
| pendingEntry.promise = null | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (entry.status === EntryStatus.Pending) { | |
| const pendingEntry = entry as PendingRouteCacheEntry | |
| pendingEntry.promise = null | |
| } | |
| const pendingEntry = entry as PendingRouteCacheEntry | |
| pendingEntry.promise = null |
The promise cleanup code in fulfillRouteCacheEntry never executes because the entry status is already changed to Fulfilled before the check, causing a memory leak where promises are never cleared.
View Details
Analysis
Memory leak: Promise not cleared in fulfillRouteCacheEntry
What fails: The fulfillRouteCacheEntry function in packages/next/src/client/components/segment-cache/cache.ts (lines 944-947) never clears the promise from memory after resolving it, causing a memory leak.
How to reproduce:
- Request a route prefetch that results in a Pending cache entry
- Call
fulfillRouteCacheEntrywith that entry - The entry's promise is resolved but never set to null
Root cause: The code captures whether the entry is Pending at line 926-929, but then at line 932 changes the status to Fulfilled. When it later checks if (entry.status === EntryStatus.Pending) at line 944, the condition is always false because the status has already been changed to Fulfilled. This prevents the cleanup code pendingEntry.promise = null from executing, keeping the resolved promise object in memory indefinitely.
Fix: Since promiseToResolve is only set if the entry was Pending at the time of the first check (line 926), we can use promiseToResolve !== null as the condition to clear the promise, rather than checking the status a second time after it has already been changed to Fulfilled.
CodSpeed Performance ReportMerging #86700 will not alter performanceComparing Summary
Footnotes
|
What?
Fixed duplicate RSC requests when navigation happens while a prefetch is already in progress. Previously, when a user clicked a link quickly after prefetch started, navigation would immediately make a new RSC request instead of waiting for the inflight prefetch to complete.
Why?
When a prefetch has
Pendingstatus (request already in flight), the navigation logic was only checking forFulfilledstatus. If the prefetch was still pending, navigation would fall through tonavigateDynamicallyWithNoPrefetch, which immediately makes a new RSC request. This caused duplicate requests and wasted network bandwidth, especially when users clicked links quickly after prefetch was initiated.How?
promisefield toPendingRouteCacheEntryto track navigation requests waiting for prefetch completionwaitForRouteCacheEntryfunction (similar towaitForSegmentCacheEntry) that returns a promise resolving when the prefetch completesfulfillRouteCacheEntryandrejectRouteCacheEntryto resolve the promise when prefetch completes or failsnavigatefunction to check forPendingstatus and wait for prefetch completion before making a new requestThe implementation follows the same pattern already used for segment cache entries, ensuring consistency across the codebase.
Fixes #86400