diff --git a/.github/workflows/build_reusable.yml b/.github/workflows/build_reusable.yml
index e873d550e3529..a45fb4ef75f36 100644
--- a/.github/workflows/build_reusable.yml
+++ b/.github/workflows/build_reusable.yml
@@ -118,6 +118,10 @@ env:
# defaults to 256, but we run a lot of tests in parallel, so the limit should be lower
NEXT_TURBOPACK_IO_CONCURRENCY: 64
+ # Disable warnings from baseline-browser-mapping
+ # https://github.com/web-platform-dx/baseline-browser-mapping/blob/ec8136ae9e034b332fab991d63a340d2e13b8afc/README.md?plain=1#L34
+ BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: 1
+
jobs:
build:
timeout-minutes: ${{ inputs.timeout_minutes }}
diff --git a/.prettierignore b/.prettierignore
index 198d0a004f3f6..de50a79a0b6ce 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -78,6 +78,7 @@ test/integration/typescript-app-type-declarations/next-env.strictRouteTypes.d.ts
/turbopack/crates/turbopack-tracing/tests/node-file-trace/test/unit/tsx/lib.tsx
/apps/docs/.source/*
+/apps/*/next-env.d.ts
# Symlink files
readme.md
diff --git a/crates/next-core/src/pages_structure.rs b/crates/next-core/src/pages_structure.rs
index ab69a8b36575c..3c05755771fa1 100644
--- a/crates/next-core/src/pages_structure.rs
+++ b/crates/next-core/src/pages_structure.rs
@@ -209,7 +209,6 @@ async fn get_pages_structure_for_root_directory(
get_pages_structure_for_directory(
dir_project_path.clone(),
next_router_path.join(name)?,
- 1,
page_extensions,
)
.to_resolved()
@@ -222,7 +221,6 @@ async fn get_pages_structure_for_root_directory(
get_pages_structure_for_directory(
dir_project_path.clone(),
next_router_path.join(name)?,
- 1,
page_extensions,
),
));
@@ -325,7 +323,6 @@ async fn get_pages_structure_for_root_directory(
async fn get_pages_structure_for_directory(
project_path: FileSystemPath,
next_router_path: FileSystemPath,
- position: u32,
page_extensions: Vc>,
) -> Result> {
let span = tracing::info_span!(
@@ -368,7 +365,6 @@ async fn get_pages_structure_for_directory(
get_pages_structure_for_directory(
dir_project_path.clone(),
next_router_path.join(name)?,
- position + 1,
page_extensions,
),
));
diff --git a/packages/next/package.json b/packages/next/package.json
index 860fac9c5fe30..1f0f6dd134e0b 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -99,7 +99,7 @@
"dependencies": {
"@next/env": "16.2.0-canary.14",
"@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.8.3",
+ "baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
diff --git a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts
index 700513e4ed00b..f58c6fe6d3216 100644
--- a/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts
+++ b/packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts
@@ -6,18 +6,19 @@ import {
convertServerPatchToFullTree,
navigateToKnownRoute,
} from '../../segment-cache/navigation'
-import { revalidateEntireCache } from '../../segment-cache/cache'
+import { invalidateSegmentCacheEntries } from '../../segment-cache/cache'
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree'
import { FreshnessPolicy } from '../ppr-navigations'
import { invalidateBfCache } from '../../segment-cache/bfcache'
export function refreshReducer(state: ReadonlyReducerState): ReducerState {
- // TODO: Currently, all refreshes purge the prefetch cache. In the future,
- // only client-side refreshes will have this behavior; the server-side
- // `refresh` should send new data without purging the prefetch cache.
+ // During a refresh, we invalidate the segment cache but not the route cache.
+ // The route cache contains the tree structure (which segments exist at a
+ // given URL) which doesn't change during a refresh. The segment cache
+ // contains the actual RSC data which needs to be re-fetched.
const currentNextUrl = state.nextUrl
const currentRouterState = state.tree
- revalidateEntireCache(currentNextUrl, currentRouterState)
+ invalidateSegmentCacheEntries(currentNextUrl, currentRouterState)
return refreshDynamicData(state, FreshnessPolicy.RefreshAll)
}
diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
index b2bcc01acd085..05899ea88a870 100644
--- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
+++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts
@@ -45,7 +45,8 @@ import {
extractInfoFromServerReferenceId,
omitUnusedArgs,
} from '../../../../shared/lib/server-reference-info'
-import { revalidateEntireCache } from '../../segment-cache/cache'
+import { invalidateEntirePrefetchCache } from '../../segment-cache/cache'
+import { startRevalidationCooldown } from '../../segment-cache/scheduler'
import { getDeploymentId } from '../../../../shared/lib/deployment-id'
import {
completeHardNavigation,
@@ -291,11 +292,19 @@ export function serverActionReducer(
// (ie, due to a navigation, before the action completed)
action.didRevalidate = true
- // If there was a revalidation, evict the entire prefetch cache.
+ // If there was a revalidation, evict the prefetch cache.
// TODO: Evict only segments with matching tags and/or paths.
+ // TODO: We should only invalidate the route cache if cookies were
+ // mutated, since route trees may vary based on cookies. For now we
+ // invalidate both caches until we have a way to detect cookie
+ // mutations on the client.
if (revalidationKind === ActionDidRevalidateStaticAndDynamic) {
- revalidateEntireCache(nextUrl, state.tree)
+ invalidateEntirePrefetchCache(nextUrl, state.tree)
}
+
+ // Start a cooldown before re-prefetching to allow CDN cache
+ // propagation.
+ startRevalidationCooldown()
}
const navigateType = redirectType || 'push'
diff --git a/packages/next/src/client/components/segment-cache/cache.ts b/packages/next/src/client/components/segment-cache/cache.ts
index 66d0e84bb6d42..d0952d040e88f 100644
--- a/packages/next/src/client/components/segment-cache/cache.ts
+++ b/packages/next/src/client/components/segment-cache/cache.ts
@@ -29,7 +29,6 @@ import {
isPrefetchTaskDirty,
type PrefetchTask,
type PrefetchSubtaskResult,
- startRevalidationCooldown,
} from './scheduler'
import {
type RouteVaryPath,
@@ -298,40 +297,69 @@ let segmentCacheMap: CacheMap = createCacheMap()
// prefetch task if desired.
let invalidationListeners: Set | null = null
-// Incrementing counter used to track cache invalidations.
-let currentCacheVersion = 0
+// Incrementing counters used to track cache invalidations. Route and segment
+// caches have separate versions so they can be invalidated independently.
+// Invalidation does not eagerly evict anything from the cache; entries are
+// lazily evicted when read.
+let currentRouteCacheVersion = 0
+let currentSegmentCacheVersion = 0
-export function getCurrentCacheVersion(): number {
- return currentCacheVersion
+export function getCurrentRouteCacheVersion(): number {
+ return currentRouteCacheVersion
+}
+
+export function getCurrentSegmentCacheVersion(): number {
+ return currentSegmentCacheVersion
}
/**
- * Used to clear the client prefetch cache when a server action calls
- * revalidatePath or revalidateTag. Eventually we will support only clearing the
- * segments that were actually affected, but there's more work to be done on the
- * server before the client is able to do this correctly.
+ * Invalidates all prefetch cache entries (both route and segment caches).
+ *
+ * After invalidation, triggers re-prefetching of visible links and notifies
+ * invalidation listeners.
*/
-export function revalidateEntireCache(
+export function invalidateEntirePrefetchCache(
nextUrl: string | null,
tree: FlightRouterState
-) {
- // Increment the current cache version. This does not eagerly evict anything
- // from the cache, but because all the entries are versioned, and we check
- // the version when reading from the cache, this effectively causes all
- // entries to be evicted lazily. We do it lazily because in the future,
- // actions like revalidateTag or refresh will not evict the entire cache,
- // but rather some subset of the entries.
- currentCacheVersion++
-
- // Start a cooldown before re-prefetching to allow CDN cache propagation.
- startRevalidationCooldown()
-
- // Prefetch all the currently visible links again, to re-fill the cache.
+): void {
+ currentRouteCacheVersion++
+ currentSegmentCacheVersion++
+
pingVisibleLinks(nextUrl, tree)
+ pingInvalidationListeners(nextUrl, tree)
+}
- // Similarly, notify all invalidation listeners (i.e. those passed to
- // `router.prefetch(onInvalidate)`), so they can trigger a new prefetch
- // if needed.
+/**
+ * Invalidates all route cache entries. Route entries contain the tree structure
+ * (which segments exist at a given URL) but not the segment data itself.
+ *
+ * After invalidation, triggers re-prefetching of visible links and notifies
+ * invalidation listeners.
+ */
+export function invalidateRouteCacheEntries(
+ nextUrl: string | null,
+ tree: FlightRouterState
+): void {
+ currentRouteCacheVersion++
+
+ pingVisibleLinks(nextUrl, tree)
+ pingInvalidationListeners(nextUrl, tree)
+}
+
+/**
+ * Invalidates all segment cache entries. Segment entries contain the actual
+ * RSC data for each segment.
+ *
+ * After invalidation, triggers re-prefetching of visible links and notifies
+ * invalidation listeners.
+ */
+export function invalidateSegmentCacheEntries(
+ nextUrl: string | null,
+ tree: FlightRouterState
+): void {
+ currentSegmentCacheVersion++
+
+ pingVisibleLinks(nextUrl, tree)
pingInvalidationListeners(nextUrl, tree)
}
@@ -401,7 +429,7 @@ export function readRouteCacheEntry(
const isRevalidation = false
return getFromCacheMap(
now,
- getCurrentCacheVersion(),
+ getCurrentRouteCacheVersion(),
routeCacheMap,
varyPath,
isRevalidation
@@ -415,7 +443,7 @@ export function readSegmentCacheEntry(
const isRevalidation = false
return getFromCacheMap(
now,
- getCurrentCacheVersion(),
+ getCurrentSegmentCacheVersion(),
segmentCacheMap,
varyPath,
isRevalidation
@@ -429,7 +457,7 @@ function readRevalidatingSegmentCacheEntry(
const isRevalidation = true
return getFromCacheMap(
now,
- getCurrentCacheVersion(),
+ getCurrentSegmentCacheVersion(),
segmentCacheMap,
varyPath,
isRevalidation
@@ -487,7 +515,7 @@ export function readOrCreateRouteCacheEntry(
// Since this is an empty entry, there's no reason to ever evict it. It will
// be updated when the data is populated.
staleAt: Infinity,
- version: getCurrentCacheVersion(),
+ version: getCurrentRouteCacheVersion(),
}
const varyPath: RouteVaryPath = getRouteVaryPath(
key.pathname,
@@ -679,16 +707,17 @@ function createOptimisticRouteTree(
export function readOrCreateSegmentCacheEntry(
now: number,
fetchStrategy: FetchStrategy,
- route: FulfilledRouteCacheEntry,
tree: RouteTree
): SegmentCacheEntry {
const existingEntry = readSegmentCacheEntry(now, tree.varyPath)
if (existingEntry !== null) {
return existingEntry
}
- // Create a pending entry and add it to the cache.
+ // Create a pending entry and add it to the cache. The stale time is set to a
+ // default value; the actual stale time will be set when the entry is
+ // fulfilled with data from the server response.
const varyPathForRequest = getSegmentVaryPathForRequest(fetchStrategy, tree)
- const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt)
+ const pendingEntry = createDetachedSegmentCacheEntry(now)
const isRevalidation = false
setInCacheMap(
segmentCacheMap,
@@ -702,7 +731,6 @@ export function readOrCreateSegmentCacheEntry(
export function readOrCreateRevalidatingSegmentEntry(
now: number,
fetchStrategy: FetchStrategy,
- route: FulfilledRouteCacheEntry,
tree: RouteTree
): SegmentCacheEntry {
// This function is called when we've already confirmed that a particular
@@ -736,9 +764,11 @@ export function readOrCreateRevalidatingSegmentEntry(
if (existingEntry !== null) {
return existingEntry
}
- // Create a pending entry and add it to the cache.
+ // Create a pending entry and add it to the cache. The stale time is set to a
+ // default value; the actual stale time will be set when the entry is
+ // fulfilled with data from the server response.
const varyPathForRequest = getSegmentVaryPathForRequest(fetchStrategy, tree)
- const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt)
+ const pendingEntry = createDetachedSegmentCacheEntry(now)
const isRevalidation = true
setInCacheMap(
segmentCacheMap,
@@ -750,15 +780,17 @@ export function readOrCreateRevalidatingSegmentEntry(
}
export function overwriteRevalidatingSegmentCacheEntry(
+ now: number,
fetchStrategy: FetchStrategy,
- route: FulfilledRouteCacheEntry,
tree: RouteTree
) {
// This function is called when we've already decided to replace an existing
// revalidation entry. Create a new entry and write it into the cache,
- // overwriting the previous value.
+ // overwriting the previous value. The stale time is set to a default value;
+ // the actual stale time will be set when the entry is fulfilled with data
+ // from the server response.
const varyPathForRequest = getSegmentVaryPathForRequest(fetchStrategy, tree)
- const pendingEntry = createDetachedSegmentCacheEntry(route.staleAt)
+ const pendingEntry = createDetachedSegmentCacheEntry(now)
const isRevalidation = true
setInCacheMap(
segmentCacheMap,
@@ -781,7 +813,7 @@ export function upsertSegmentEntry(
// since the request was made. We can do that by passing the "owner" entry to
// this function and confirming it's the same as `existingEntry`.
- if (isValueExpired(now, getCurrentCacheVersion(), candidateEntry)) {
+ if (isValueExpired(now, getCurrentSegmentCacheVersion(), candidateEntry)) {
// The entry is expired. We cannot upsert it.
return null
}
@@ -824,8 +856,11 @@ export function upsertSegmentEntry(
}
export function createDetachedSegmentCacheEntry(
- staleAt: number
+ now: number
): EmptySegmentCacheEntry {
+ // Default stale time for pending segment cache entries. The actual stale time
+ // is set when the entry is fulfilled with data from the server response.
+ const staleAt = now + 30 * 1000
const emptyEntry: EmptySegmentCacheEntry = {
status: EntryStatus.Empty,
// Default to assuming the fetch strategy will be PPR. This will be updated
@@ -860,11 +895,11 @@ export function upgradeToPendingSegment(
}
// Set the version here, since this is right before the request is initiated.
- // The next time the global cache version is incremented, the entry will
+ // The next time the segment cache version is incremented, the entry will
// effectively be evicted. This happens before initiating the request, rather
// than when receiving the response, because it's guaranteed to happen
// before the data is read on the server.
- pendingEntry.version = getCurrentCacheVersion()
+ pendingEntry.version = getCurrentSegmentCacheVersion()
return pendingEntry
}
@@ -926,10 +961,10 @@ function pingBlockedTasks(entry: {
}
function fulfillRouteCacheEntry(
+ now: number,
entry: RouteCacheEntry,
tree: RouteTree,
metadataVaryPath: PageVaryPath,
- staleAt: number,
couldBeIntercepted: boolean,
canonicalUrl: string,
renderedSearch: NormalizedSearch,
@@ -957,7 +992,11 @@ function fulfillRouteCacheEntry(
fulfilledEntry.status = EntryStatus.Fulfilled
fulfilledEntry.tree = tree
fulfilledEntry.metadata = metadata
- fulfilledEntry.staleAt = staleAt
+ // Route structure is essentially static — it only changes on deploy.
+ // Always use the static stale time.
+ // NOTE: An exception is rewrites/redirects in middleware or proxy, which can
+ // change routes dynamically. We have other strategies for handling those.
+ fulfilledEntry.staleAt = now + STATIC_STALETIME_MS
fulfilledEntry.couldBeIntercepted = couldBeIntercepted
fulfilledEntry.canonicalUrl = canonicalUrl
fulfilledEntry.renderedSearch = renderedSearch
@@ -1583,12 +1622,11 @@ export async function fetchRouteOnCacheMiss(
return null
}
- const staleTimeMs = getStaleTimeMs(serverData.staleTime)
fulfillRouteCacheEntry(
+ Date.now(),
entry,
routeTree,
metadataVaryPath,
- Date.now() + staleTimeMs,
couldBeIntercepted,
canonicalUrl,
renderedSearch,
@@ -1765,13 +1803,12 @@ export async function fetchSegmentOnCacheMiss(
rejectSegmentCacheEntry(segmentCacheEntry, Date.now() + 10 * 1000)
return null
}
+ const staleAt = Date.now() + getStaleTimeMs(serverData.staleTime)
return {
value: fulfillSegmentCacheEntry(
segmentCacheEntry,
serverData.rsc,
- // TODO: The server does not currently provide per-segment stale time.
- // So we use the stale time of the route.
- route.staleAt,
+ staleAt,
serverData.isPartial
),
// Return a promise that resolves when the network connection closes, so
@@ -1953,16 +1990,6 @@ function writeDynamicTreeResponseIntoCache(
}
const flightRouterState = flightData.tree
- // For runtime prefetches, stale time is in the payload at rp[1].
- // For other responses, fall back to the header.
- const staleTimeSeconds =
- typeof serverData.rp?.[1] === 'number'
- ? serverData.rp[1]
- : parseInt(response.headers.get(NEXT_ROUTER_STALE_TIME_HEADER) ?? '', 10)
- const staleTimeMs = !isNaN(staleTimeSeconds)
- ? getStaleTimeMs(staleTimeSeconds)
- : STATIC_STALETIME_MS
-
// If the response contains dynamic holes, then we must conservatively assume
// that any individual segment might contain dynamic holes, and also the
// head. If it did not contain dynamic holes, then we can assume every segment
@@ -1988,10 +2015,10 @@ function writeDynamicTreeResponseIntoCache(
}
const fulfilledEntry = fulfillRouteCacheEntry(
+ now,
entry,
routeTree,
metadataVaryPath,
- now + staleTimeMs,
couldBeIntercepted,
canonicalUrl,
renderedSearch,
@@ -2105,7 +2132,6 @@ function writeDynamicRenderResponseIntoCache(
now,
task,
fetchStrategy,
- route,
tree,
staleAt,
seedData,
@@ -2119,7 +2145,6 @@ function writeDynamicRenderResponseIntoCache(
fulfillEntrySpawnedByRuntimePrefetch(
now,
fetchStrategy,
- route,
head,
flightData.isHeadPartial,
staleAt,
@@ -2153,7 +2178,6 @@ function writeSeedDataIntoCache(
| FetchStrategy.LoadingBoundary
| FetchStrategy.PPRRuntime
| FetchStrategy.Full,
- route: FulfilledRouteCacheEntry,
tree: RouteTree,
staleAt: number,
seedData: CacheNodeSeedData,
@@ -2170,7 +2194,6 @@ function writeSeedDataIntoCache(
fulfillEntrySpawnedByRuntimePrefetch(
now,
fetchStrategy,
- route,
rsc,
isPartial,
staleAt,
@@ -2191,7 +2214,6 @@ function writeSeedDataIntoCache(
now,
task,
fetchStrategy,
- route,
childTree,
staleAt,
childSeedData,
@@ -2209,7 +2231,6 @@ function fulfillEntrySpawnedByRuntimePrefetch(
| FetchStrategy.LoadingBoundary
| FetchStrategy.PPRRuntime
| FetchStrategy.Full,
- route: FulfilledRouteCacheEntry,
rsc: React.ReactNode,
isPartial: boolean,
staleAt: number,
@@ -2233,7 +2254,6 @@ function fulfillEntrySpawnedByRuntimePrefetch(
const possiblyNewEntry = readOrCreateSegmentCacheEntry(
now,
fetchStrategy,
- route,
tree
)
if (possiblyNewEntry.status === EntryStatus.Empty) {
@@ -2250,7 +2270,7 @@ function fulfillEntrySpawnedByRuntimePrefetch(
// replace it with the new one from the server.
const newEntry = fulfillSegmentCacheEntry(
upgradeToPendingSegment(
- createDetachedSegmentCacheEntry(staleAt),
+ createDetachedSegmentCacheEntry(now),
fetchStrategy
),
rsc,
diff --git a/packages/next/src/client/components/segment-cache/scheduler.ts b/packages/next/src/client/components/segment-cache/scheduler.ts
index 348d2a29c95a1..94fe0d43953c2 100644
--- a/packages/next/src/client/components/segment-cache/scheduler.ts
+++ b/packages/next/src/client/components/segment-cache/scheduler.ts
@@ -35,7 +35,10 @@ import {
type PrefetchTaskFetchStrategy,
PrefetchPriority,
} from './types'
-import { getCurrentCacheVersion } from './cache'
+import {
+ getCurrentRouteCacheVersion,
+ getCurrentSegmentCacheVersion,
+} from './cache'
import {
addSearchParamsIfPageSegment,
PAGE_SEGMENT_KEY,
@@ -65,10 +68,13 @@ export type PrefetchTask = {
treeAtTimeOfPrefetch: FlightRouterState
/**
- * The cache version at the time the task was initiated. This is used to
- * determine if the cache was invalidated since the task was initiated.
+ * The cache versions at the time the task was initiated. Used to determine
+ * if the cache was invalidated since the task was initiated. Route and
+ * segment caches have separate versions so they can be invalidated
+ * independently.
*/
- cacheVersion: number
+ routeCacheVersion: number
+ segmentCacheVersion: number
/**
* Whether to prefetch dynamic data, in addition to static data. This is
@@ -249,7 +255,8 @@ export function schedulePrefetchTask(
const task: PrefetchTask = {
key,
treeAtTimeOfPrefetch,
- cacheVersion: getCurrentCacheVersion(),
+ routeCacheVersion: getCurrentRouteCacheVersion(),
+ segmentCacheVersion: getCurrentSegmentCacheVersion(),
priority,
phase: PrefetchPhase.RouteTree,
hasBackgroundWork: false,
@@ -336,9 +343,9 @@ export function isPrefetchTaskDirty(
// strictly an optimization — theoretically, if it always returned true, no
// behavior should change because a full prefetch task will effectively
// perform the same checks.
- const currentCacheVersion = getCurrentCacheVersion()
return (
- task.cacheVersion !== currentCacheVersion ||
+ task.routeCacheVersion !== getCurrentRouteCacheVersion() ||
+ task.segmentCacheVersion !== getCurrentSegmentCacheVersion() ||
task.treeAtTimeOfPrefetch !== tree ||
task.key.nextUrl !== nextUrl
)
@@ -477,7 +484,8 @@ function processQueueInMicrotask() {
// Process the task queue until we run out of network bandwidth.
let task = heapPeek(taskHeap)
while (task !== null && hasNetworkBandwidth(task)) {
- task.cacheVersion = getCurrentCacheVersion()
+ task.routeCacheVersion = getCurrentRouteCacheVersion()
+ task.segmentCacheVersion = getCurrentSegmentCacheVersion()
const exitStatus = pingRoute(now, task)
@@ -788,12 +796,7 @@ function pingStaticHead(
now,
task,
route,
- readOrCreateSegmentCacheEntry(
- now,
- FetchStrategy.PPR,
- route,
- route.metadata
- ),
+ readOrCreateSegmentCacheEntry(now, FetchStrategy.PPR, route.metadata),
task.key,
route.metadata
)
@@ -848,7 +851,6 @@ function pingSharedPartOfCacheComponentsTree(
const segment = readOrCreateSegmentCacheEntry(
now,
task.fetchStrategy,
- route,
newTree
)
pingStaticSegmentData(now, task, route, segment, task.key, newTree)
@@ -946,12 +948,7 @@ function pingNewPartOfCacheComponentsTree(
}
// This segment should not be runtime prefetched. Prefetch its static data.
- const segment = readOrCreateSegmentCacheEntry(
- now,
- task.fetchStrategy,
- route,
- tree
- )
+ const segment = readOrCreateSegmentCacheEntry(now, task.fetchStrategy, tree)
pingStaticSegmentData(now, task, route, segment, task.key, tree)
if (tree.slots !== null) {
if (!hasNetworkBandwidth(task)) {
@@ -1146,12 +1143,7 @@ function pingPPRDisabledRouteTreeUpToLoadingBoundary(
let refetchMarker: 'refetch' | 'inside-shared-layout' | null =
refetchMarkerContext === null ? 'inside-shared-layout' : null
- const segment = readOrCreateSegmentCacheEntry(
- now,
- task.fetchStrategy,
- route,
- tree
- )
+ const segment = readOrCreateSegmentCacheEntry(now, task.fetchStrategy, tree)
switch (segment.status) {
case EntryStatus.Empty: {
// This segment is not cached. Add a refetch marker so the server knows
@@ -1262,7 +1254,6 @@ function pingRouteTreeAndIncludeDynamicData(
// always use runtime prefetching (via `export const prefetch`), and those should check for
// entries that include search params.
fetchStrategy,
- route,
tree
)
@@ -1301,12 +1292,7 @@ function pingRouteTreeAndIncludeDynamicData(
// - we have a static prefetch, and we're doing a runtime prefetch
// - we have a static or runtime prefetch, and we're doing a Full prefetch (or a navigation).
// In either case, we need to include it in the request to get a more specific (or full) version.
- spawnedSegment = pingFullSegmentRevalidation(
- now,
- route,
- tree,
- fetchStrategy
- )
+ spawnedSegment = pingFullSegmentRevalidation(now, tree, fetchStrategy)
}
break
}
@@ -1320,12 +1306,7 @@ function pingRouteTreeAndIncludeDynamicData(
fetchStrategy
)
) {
- spawnedSegment = pingFullSegmentRevalidation(
- now,
- route,
- tree,
- fetchStrategy
- )
+ spawnedSegment = pingFullSegmentRevalidation(now, tree, fetchStrategy)
}
break
}
@@ -1514,7 +1495,6 @@ function pingPPRSegmentRevalidation(
const revalidatingSegment = readOrCreateRevalidatingSegmentEntry(
now,
FetchStrategy.PPR,
- route,
tree
)
switch (revalidatingSegment.status) {
@@ -1549,14 +1529,12 @@ function pingPPRSegmentRevalidation(
function pingFullSegmentRevalidation(
now: number,
- route: FulfilledRouteCacheEntry,
tree: RouteTree,
fetchStrategy: FetchStrategy.Full | FetchStrategy.PPRRuntime
): PendingSegmentCacheEntry | null {
const revalidatingSegment = readOrCreateRevalidatingSegmentEntry(
now,
fetchStrategy,
- route,
tree
)
if (revalidatingSegment.status === EntryStatus.Empty) {
@@ -1586,8 +1564,8 @@ function pingFullSegmentRevalidation(
// The existing revalidation was fetched using a less specific strategy.
// Reset it and start a new revalidation.
const emptySegment = overwriteRevalidatingSegmentCacheEntry(
+ now,
fetchStrategy,
- route,
tree
)
const pendingSegment = upgradeToPendingSegment(
diff --git a/packages/next/src/server/app-render/collect-segment-data.tsx b/packages/next/src/server/app-render/collect-segment-data.tsx
index bec0d56055d4c..b9d46dc707864 100644
--- a/packages/next/src/server/app-render/collect-segment-data.tsx
+++ b/packages/next/src/server/app-render/collect-segment-data.tsx
@@ -80,6 +80,7 @@ export type SegmentPrefetch = {
buildId: string
rsc: React.ReactNode | null
isPartial: boolean
+ staleTime: number
}
const filterStackFrame =
@@ -241,6 +242,7 @@ async function PrefetchTreeData({
isClientParamParsingEnabled,
flightRouterState,
buildId,
+ staleTime,
seedData,
clientModules,
ROOT_SEGMENT_REQUEST_KEY,
@@ -253,7 +255,13 @@ async function PrefetchTreeData({
// the client cache.
segmentTasks.push(
waitAtLeastOneReactRenderTask().then(() =>
- renderSegmentPrefetch(buildId, head, HEAD_REQUEST_KEY, clientModules)
+ renderSegmentPrefetch(
+ buildId,
+ staleTime,
+ head,
+ HEAD_REQUEST_KEY,
+ clientModules
+ )
)
)
@@ -275,6 +283,7 @@ function collectSegmentDataImpl(
isClientParamParsingEnabled: boolean,
route: FlightRouterState,
buildId: string,
+ staleTime: number,
seedData: CacheNodeSeedData | null,
clientModules: ManifestNode,
requestKey: SegmentRequestKey,
@@ -301,6 +310,7 @@ function collectSegmentDataImpl(
isClientParamParsingEnabled,
childRoute,
buildId,
+ staleTime,
childSeedData,
clientModules,
childRequestKey,
@@ -320,7 +330,13 @@ function collectSegmentDataImpl(
// Since we're already in the middle of a render, wait until after the
// current task to escape the current rendering context.
waitAtLeastOneReactRenderTask().then(() =>
- renderSegmentPrefetch(buildId, seedData[0], requestKey, clientModules)
+ renderSegmentPrefetch(
+ buildId,
+ staleTime,
+ seedData[0],
+ requestKey,
+ clientModules
+ )
)
)
} else {
@@ -361,17 +377,17 @@ function collectSegmentDataImpl(
async function renderSegmentPrefetch(
buildId: string,
+ staleTime: number,
rsc: React.ReactNode,
requestKey: SegmentRequestKey,
clientModules: ManifestNode
): Promise<[SegmentRequestKey, Buffer]> {
// Render the segment data to a stream.
- // In the future, this is where we can include additional metadata, like the
- // stale time and cache tags.
const segmentPrefetch: SegmentPrefetch = {
buildId,
rsc,
isPartial: await isPartialRSCData(rsc, clientModules),
+ staleTime,
}
// Since all we're doing is decoding and re-encoding a cached prerender, if
// it takes longer than a microtask, it must because of hanging promises
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e670db8c0f1f5..2d99652ac37b7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1091,8 +1091,8 @@ importers:
specifier: 0.5.15
version: 0.5.15
baseline-browser-mapping:
- specifier: ^2.8.3
- version: 2.8.32
+ specifier: ^2.9.19
+ version: 2.9.19
caniuse-lite:
specifier: 1.0.30001746
version: 1.0.30001746
@@ -7811,8 +7811,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
- baseline-browser-mapping@2.8.32:
- resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
+ baseline-browser-mapping@2.9.19:
+ resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
batch@0.6.1:
@@ -25413,7 +25413,7 @@ snapshots:
base64-js@1.5.1: {}
- baseline-browser-mapping@2.8.32: {}
+ baseline-browser-mapping@2.9.19: {}
batch@0.6.1: {}
@@ -25607,7 +25607,7 @@ snapshots:
browserslist@4.28.0:
dependencies:
- baseline-browser-mapping: 2.8.32
+ baseline-browser-mapping: 2.9.19
caniuse-lite: 1.0.30001746
electron-to-chromium: 1.5.262
node-releases: 2.0.27
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/page.tsx
index 617f16b7a2e1d..056b5868fa8cf 100644
--- a/test/e2e/app-dir/segment-cache/staleness/app/page.tsx
+++ b/test/e2e/app-dir/segment-cache/staleness/app/page.tsx
@@ -11,23 +11,23 @@ export default function Page() {
-
-
- Page with stale time of 5 minutes
+
+ Page with stale time of 2 minutes
-
-
- Page with stale time of 10 minutes
+
+ Page with stale time of 4 minutes
-
-
- Page whose runtime prefetch has a stale time of 5 minutes
+
+ Page whose runtime prefetch has a stale time of 2 minutes
-
-
- Page whose runtime prefetch has a stale time of 10 minutes
+
+ Page whose runtime prefetch has a stale time of 4 minutes
-
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-5-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-2-minutes/page.tsx
similarity index 89%
rename from test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-5-minutes/page.tsx
rename to test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-2-minutes/page.tsx
index 7baec63e1cad3..763c125531b45 100644
--- a/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-5-minutes/page.tsx
+++ b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-2-minutes/page.tsx
@@ -27,6 +27,6 @@ async function RuntimePrefetchable() {
async function Content() {
'use cache'
await new Promise((resolve) => setTimeout(resolve, 0))
- cacheLife({ stale: 5 * 60 })
- return 'Content with stale time of 5 minutes'
+ cacheLife({ stale: 2 * 60 })
+ return 'Content with stale time of 2 minutes'
}
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-10-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-4-minutes/page.tsx
similarity index 89%
rename from test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-10-minutes/page.tsx
rename to test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-4-minutes/page.tsx
index 948c6d19d6ec3..4639fbbaa66a1 100644
--- a/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-10-minutes/page.tsx
+++ b/test/e2e/app-dir/segment-cache/staleness/app/runtime-stale-4-minutes/page.tsx
@@ -27,6 +27,6 @@ async function RuntimePrefetchable() {
async function Content() {
'use cache'
await new Promise((resolve) => setTimeout(resolve, 0))
- cacheLife({ stale: 10 * 60 })
- return 'Content with stale time of 10 minutes'
+ cacheLife({ stale: 4 * 60 })
+ return 'Content with stale time of 4 minutes'
}
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/stale-5-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/stale-2-minutes/page.tsx
similarity index 78%
rename from test/e2e/app-dir/segment-cache/staleness/app/stale-5-minutes/page.tsx
rename to test/e2e/app-dir/segment-cache/staleness/app/stale-2-minutes/page.tsx
index 125bd86603dd2..01ad244f6cfbe 100644
--- a/test/e2e/app-dir/segment-cache/staleness/app/stale-5-minutes/page.tsx
+++ b/test/e2e/app-dir/segment-cache/staleness/app/stale-2-minutes/page.tsx
@@ -4,8 +4,8 @@ import { cacheLife } from 'next/cache'
async function Content() {
'use cache'
await new Promise((resolve) => setTimeout(resolve, 0))
- cacheLife({ stale: 5 * 60 })
- return 'Content with stale time of 5 minutes'
+ cacheLife({ stale: 2 * 60 })
+ return 'Content with stale time of 2 minutes'
}
export default function Page() {
diff --git a/test/e2e/app-dir/segment-cache/staleness/app/stale-10-minutes/page.tsx b/test/e2e/app-dir/segment-cache/staleness/app/stale-4-minutes/page.tsx
similarity index 78%
rename from test/e2e/app-dir/segment-cache/staleness/app/stale-10-minutes/page.tsx
rename to test/e2e/app-dir/segment-cache/staleness/app/stale-4-minutes/page.tsx
index be402b05bbe6b..ec279549c9907 100644
--- a/test/e2e/app-dir/segment-cache/staleness/app/stale-10-minutes/page.tsx
+++ b/test/e2e/app-dir/segment-cache/staleness/app/stale-4-minutes/page.tsx
@@ -4,8 +4,8 @@ import { cacheLife } from 'next/cache'
async function Content() {
'use cache'
await new Promise((resolve) => setTimeout(resolve, 0))
- cacheLife({ stale: 10 * 60 })
- return 'Content with stale time of 10 minutes'
+ cacheLife({ stale: 4 * 60 })
+ return 'Content with stale time of 4 minutes'
}
export default function Page() {
diff --git a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts
index d33614c7efea7..3f2b0ade53b48 100644
--- a/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts
+++ b/test/e2e/app-dir/segment-cache/staleness/segment-cache-stale-time.test.ts
@@ -23,57 +23,57 @@ describe('segment cache (staleness)', () => {
await page.clock.install()
// Reveal the link to trigger a prefetch
- const toggle5MinutesLink = await browser.elementByCss(
- 'input[data-link-accordion="/stale-5-minutes"]'
+ const toggle2MinutesLink = await browser.elementByCss(
+ 'input[data-link-accordion="/stale-2-minutes"]'
)
- const toggle10MinutesLink = await browser.elementByCss(
- 'input[data-link-accordion="/stale-10-minutes"]'
+ const toggle4MinutesLink = await browser.elementByCss(
+ 'input[data-link-accordion="/stale-4-minutes"]'
)
await act(
async () => {
- await toggle5MinutesLink.click()
- await browser.elementByCss('a[href="/stale-5-minutes"]')
+ await toggle2MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-2-minutes"]')
},
{
- includes: 'Content with stale time of 5 minutes',
+ includes: 'Content with stale time of 2 minutes',
}
)
await act(
async () => {
- await toggle10MinutesLink.click()
- await browser.elementByCss('a[href="/stale-10-minutes"]')
+ await toggle4MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-4-minutes"]')
},
{
- includes: 'Content with stale time of 10 minutes',
+ includes: 'Content with stale time of 4 minutes',
}
)
// Hide the links
- await toggle5MinutesLink.click()
- await toggle10MinutesLink.click()
+ await toggle2MinutesLink.click()
+ await toggle4MinutesLink.click()
- // Fast forward 5 minutes and 1 millisecond
- await page.clock.fastForward(5 * 60 * 1000 + 1)
+ // Fast forward 2 minutes and 1 millisecond
+ await page.clock.fastForward(2 * 60 * 1000 + 1)
// Reveal the links again to trigger new prefetch tasks
await act(
async () => {
- await toggle5MinutesLink.click()
- await browser.elementByCss('a[href="/stale-5-minutes"]')
+ await toggle2MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-2-minutes"]')
},
- // The page with a stale time of 5 minutes is requested again
+ // The page with a stale time of 2 minutes is requested again
// because its stale time elapsed.
{
- includes: 'Content with stale time of 5 minutes',
+ includes: 'Content with stale time of 2 minutes',
}
)
await act(
async () => {
- await toggle10MinutesLink.click()
- await browser.elementByCss('a[href="/stale-10-minutes"]')
+ await toggle4MinutesLink.click()
+ await browser.elementByCss('a[href="/stale-4-minutes"]')
},
- // The page with a stale time of 10 minutes is *not* requested again
+ // The page with a stale time of 4 minutes is *not* requested again
// because it's still fresh.
'no-requests'
)
@@ -91,57 +91,57 @@ describe('segment cache (staleness)', () => {
await page.clock.install()
// Reveal the links to trigger a runtime prefetch
- const toggle5MinutesLink = await browser.elementByCss(
- 'input[data-link-accordion="/runtime-stale-5-minutes"]'
+ const toggle2MinutesLink = await browser.elementByCss(
+ 'input[data-link-accordion="/runtime-stale-2-minutes"]'
)
- const toggle10MinutesLink = await browser.elementByCss(
- 'input[data-link-accordion="/runtime-stale-10-minutes"]'
+ const toggle4MinutesLink = await browser.elementByCss(
+ 'input[data-link-accordion="/runtime-stale-4-minutes"]'
)
await act(
async () => {
- await toggle5MinutesLink.click()
- await browser.elementByCss('a[href="/runtime-stale-5-minutes"]')
+ await toggle2MinutesLink.click()
+ await browser.elementByCss('a[href="/runtime-stale-2-minutes"]')
},
{
- includes: 'Content with stale time of 5 minutes',
+ includes: 'Content with stale time of 2 minutes',
}
)
await act(
async () => {
- await toggle10MinutesLink.click()
- await browser.elementByCss('a[href="/runtime-stale-10-minutes"]')
+ await toggle4MinutesLink.click()
+ await browser.elementByCss('a[href="/runtime-stale-4-minutes"]')
},
{
- includes: 'Content with stale time of 10 minutes',
+ includes: 'Content with stale time of 4 minutes',
}
)
// Hide the links
- await toggle5MinutesLink.click()
- await toggle10MinutesLink.click()
+ await toggle2MinutesLink.click()
+ await toggle4MinutesLink.click()
- // Fast forward 5 minutes and 1 millisecond
- await page.clock.fastForward(5 * 60 * 1000 + 1)
+ // Fast forward 2 minutes and 1 millisecond
+ await page.clock.fastForward(2 * 60 * 1000 + 1)
// Reveal the links again to trigger new prefetch tasks
await act(
async () => {
- await toggle5MinutesLink.click()
- await browser.elementByCss('a[href="/runtime-stale-5-minutes"]')
+ await toggle2MinutesLink.click()
+ await browser.elementByCss('a[href="/runtime-stale-2-minutes"]')
},
- // The page with a stale time of 5 minutes is requested again
+ // The page with a stale time of 2 minutes is requested again
// because its stale time elapsed.
{
- includes: 'Content with stale time of 5 minutes',
+ includes: 'Content with stale time of 2 minutes',
}
)
await act(
async () => {
- await toggle10MinutesLink.click()
- await browser.elementByCss('a[href="/runtime-stale-10-minutes"]')
+ await toggle4MinutesLink.click()
+ await browser.elementByCss('a[href="/runtime-stale-4-minutes"]')
},
- // The page with a stale time of 10 minutes is *not* requested again
+ // The page with a stale time of 4 minutes is *not* requested again
// because it's still fresh.
'no-requests'
)