diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx
index dabfb3b7a066b..1bc1eb4a2e2e5 100644
--- a/packages/next/src/client/components/app-router.tsx
+++ b/packages/next/src/client/components/app-router.tsx
@@ -54,6 +54,8 @@ import type { FlightRouterState } from '../../server/app-render/types'
import { useNavFailureHandler } from './nav-failure-handler'
import { useServerActionDispatcher } from '../app-call-server'
import type { AppRouterActionQueue } from '../../shared/lib/router/action-queue'
+import { prefetch as prefetchWithSegmentCache } from '../components/segment-cache/prefetch'
+
import {
getRedirectTypeFromError,
getURLFromRedirectError,
@@ -69,6 +71,43 @@ function isExternalURL(url: URL) {
return url.origin !== window.location.origin
}
+/**
+ * Given a link href, constructs the URL that should be prefetched. Returns null
+ * in cases where prefetching should be disabled, like external URLs, or
+ * during development.
+ * @param href The href passed to , router.prefetch(), or similar
+ * @returns A URL object to prefetch, or null if prefetching should be disabled
+ */
+export function createPrefetchURL(href: string): URL | null {
+ // Don't prefetch for bots as they don't navigate.
+ if (isBot(window.navigator.userAgent)) {
+ return null
+ }
+
+ let url: URL
+ try {
+ url = new URL(addBasePath(href), window.location.href)
+ } catch (_) {
+ // TODO: Does this need to throw or can we just console.error instead? Does
+ // anyone rely on this throwing? (Seems unlikely.)
+ throw new Error(
+ `Cannot prefetch '${href}' because it cannot be converted to a URL.`
+ )
+ }
+
+ // Don't prefetch during development (improves compilation performance)
+ if (process.env.NODE_ENV === 'development') {
+ return null
+ }
+
+ // External urls can't be prefetched in the same way.
+ if (isExternalURL(url)) {
+ return null
+ }
+
+ return url
+}
+
function HistoryUpdater({
appRouterState,
}: {
@@ -241,38 +280,25 @@ function Router({
const routerInstance: AppRouterInstance = {
back: () => window.history.back(),
forward: () => window.history.forward(),
- prefetch: (href, options) => {
- // Don't prefetch for bots as they don't navigate.
- if (isBot(window.navigator.userAgent)) {
- return
- }
-
- let url: URL
- try {
- url = new URL(addBasePath(href), window.location.href)
- } catch (_) {
- throw new Error(
- `Cannot prefetch '${href}' because it cannot be converted to a URL.`
- )
- }
-
- // Don't prefetch during development (improves compilation performance)
- if (process.env.NODE_ENV === 'development') {
- return
- }
-
- // External urls can't be prefetched in the same way.
- if (isExternalURL(url)) {
- return
- }
- startTransition(() => {
- dispatch({
- type: ACTION_PREFETCH,
- url,
- kind: options?.kind ?? PrefetchKind.FULL,
- })
- })
- },
+ prefetch:
+ process.env.__NEXT_PPR && process.env.__NEXT_CLIENT_SEGMENT_CACHE
+ ? // Unlike the old implementation, the Segment Cache doesn't store its
+ // data in the router reducer state; it writes into a global mutable
+ // cache. So we don't need to dispatch an action.
+ prefetchWithSegmentCache
+ : (href, options) => {
+ // Use the old prefetch implementation.
+ const url = createPrefetchURL(href)
+ if (url !== null) {
+ startTransition(() => {
+ dispatch({
+ type: ACTION_PREFETCH,
+ url,
+ kind: options?.kind ?? PrefetchKind.FULL,
+ })
+ })
+ }
+ },
replace: (href, options = {}) => {
startTransition(() => {
navigate(href, 'replace', options.scroll ?? true)
diff --git a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts
index ed05a573c454d..8edd75bbfb3bf 100644
--- a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts
+++ b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts
@@ -3,7 +3,6 @@ import type {
ReducerState,
ReadonlyReducerState,
} from '../router-reducer-types'
-import { NEXT_RSC_UNION_QUERY } from '../../app-router-headers'
import { PromiseQueue } from '../../promise-queue'
import {
getOrCreatePrefetchCacheEntry,
@@ -12,7 +11,22 @@ import {
export const prefetchQueue = new PromiseQueue(5)
-export function prefetchReducer(
+export const prefetchReducer =
+ process.env.__NEXT_PPR && process.env.__NEXT_CLIENT_SEGMENT_CACHE
+ ? identityReducerWhenSegmentCacheIsEnabled
+ : prefetchReducerImpl
+
+function identityReducerWhenSegmentCacheIsEnabled(state: T): T {
+ // Unlike the old implementation, the Segment Cache doesn't store its data in
+ // the router reducer state.
+ //
+ // This shouldn't be reachable because we wrap the prefetch API in a check,
+ // too, which prevents the action from being dispatched. But it's here for
+ // clarity + code elimination.
+ return state
+}
+
+function prefetchReducerImpl(
state: ReadonlyReducerState,
action: PrefetchAction
): ReducerState {
@@ -20,7 +34,6 @@ export function prefetchReducer(
prunePrefetchCache(state.prefetchCache)
const { url } = action
- url.searchParams.delete(NEXT_RSC_UNION_QUERY)
getOrCreatePrefetchCacheEntry({
url,
diff --git a/packages/next/src/client/components/segment-cache/prefetch.ts b/packages/next/src/client/components/segment-cache/prefetch.ts
new file mode 100644
index 0000000000000..384c905c33493
--- /dev/null
+++ b/packages/next/src/client/components/segment-cache/prefetch.ts
@@ -0,0 +1,16 @@
+import { createPrefetchURL } from '../../components/app-router'
+
+/**
+ * Entrypoint for prefetching a URL into the Segment Cache.
+ * @param href - The URL to prefetch. Typically this will come from a ,
+ * or router.prefetch. It must be validated before we attempt to prefetch it.
+ */
+export function prefetch(href: string) {
+ const url = createPrefetchURL(href)
+ if (url === null) {
+ // This href should not be prefetched.
+ return
+ }
+
+ // TODO: Not yet implemented
+}