Skip to content

Conversation

@rosbitskyy
Copy link

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 Pending status (request already in flight), the navigation logic was only checking for Fulfilled status. If the prefetch was still pending, navigation would fall through to navigateDynamicallyWithNoPrefetch, 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?

  • Added promise field to PendingRouteCacheEntry to track navigation requests waiting for prefetch completion
  • Created waitForRouteCacheEntry function (similar to waitForSegmentCacheEntry) that returns a promise resolving when the prefetch completes
  • Updated fulfillRouteCacheEntry and rejectRouteCacheEntry to resolve the promise when prefetch completes or fails
  • Modified navigate function to check for Pending status and wait for prefetch completion before making a new request
  • Added e2e test to verify only one request is made when navigation happens during inflight prefetch

The implementation follows the same pattern already used for segment cache entries, ensuring consistency across the codebase.

Fixes #86400

…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
@ijjk ijjk added tests Turbopack Related to Turbopack with Next.js. type: next labels Dec 1, 2025
@vercel
Copy link
Contributor

vercel bot commented Dec 1, 2025

Notifying the following users due to files changed in this PR based on this repo's notify modifiers:

@timneutkens, @ijjk, @shuding, @huozhi:

packages/next/src/server/config.ts

@ijjk
Copy link
Member

ijjk commented Dec 1, 2025

Allow CI Workflow Run

  • approve CI run for commit: 6a88ec2

Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer

Comment on lines +941 to +948
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
}
}
Copy link
Contributor

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
}
Suggested change
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

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +944 to +947
if (entry.status === EntryStatus.Pending) {
const pendingEntry = entry as PendingRouteCacheEntry
pendingEntry.promise = null
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:

  1. Request a route prefetch that results in a Pending cache entry
  2. Call fulfillRouteCacheEntry with that entry
  3. 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-hq
Copy link

codspeed-hq bot commented Dec 1, 2025

CodSpeed Performance Report

Merging #86700 will not alter performance

Comparing rosbitskyy:fix/prefetch-inflight-duplicate (6a88ec2) with canary (2cd70df)

Summary

✅ 17 untouched
⏩ 3 skipped1

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Turbopack Related to Turbopack with Next.js. type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prefetch full refetch the payload if there is already an inflight on Next 16

2 participants