Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
Partial Prerendering (PPR) and prefetching form the architectural backbone of Next.js client-side navigation, optimizing application responsiveness by separating static shells from dynamic data streams. By combining viewport observation with priority heap task scheduling, Next.js proactively prefetches route trees and individual segment bundles before a user navigates, eliminating round-trip latency. Sources: packages/next/src/client/components/segment-cache/scheduler.ts:621-627, packages/next/src/client/components/links.ts:249-300
The Segment Cache orchestrates the storage, LRU retention, dynamic staleness computation, and mutation-driven invalidation of cached route elements. Optimistic routing exploits pattern discovery to match client route trees instantly, falling back to server resolution or rewrite handling when mismatches occur. Sources: packages/next/src/client/components/segment-cache/cache.ts:3081-3110, packages/next/src/client/components/segment-cache/optimistic-routes.ts:1-44
During navigation, PPR executes immediate shell rendering alongside deferred dynamic RSC fetches, reconciling router trees via copy-on-write task updates and staged payloads. Integration with the browser's back-forward cache preserves session history and dynamic segment states, while synchronization locks and development debug channels secure navigation execution and diagnostic telemetry. Sources: packages/next/src/client/components/router-reducer/ppr-navigations.ts:163-190, packages/next/src/client/components/segment-cache/bfcache.ts:32-57, packages/next/src/client/dev/debug-channel.ts:307-359
Link prefetching and task scheduling manage when and how navigation targets are identified, observed for visibility, and prioritized for prefetching. By using a shared IntersectionObserver across all <Link> components with a root margin of 200px, Next.js observes when anchor tags enter or approach the viewport. When visibility status updates or a navigation intent is triggered via user interaction, link instances coordinate with the segment cache scheduler to initiate or reschedule background prefetch tasks.
Sources: packages/next/src/client/components/links.ts:107-141, packages/next/src/client/components/links.ts:249-300
Link instances are registered using mountLinkInstance or mountFormInstance, storing references inside a prefetchable collection tracked by an IntersectionObserver. The observation flow proceeds through specific function calls:
handleIntersect() receives entries from the observer and determines visibility via entry.intersectionRatio > 0.onLinkVisibilityChanged() updates instance.isVisible, adds or removes the instance from prefetchableAndVisible, and calls rescheduleLinkPrefetch() with PrefetchPriority.Default.onNavigationIntent() is invoked on hover or touch events, optionally upgrading the fetch strategy to FetchStrategy.Full when __NEXT_DYNAMIC_ON_HOVER and unstable_upgradeToDynamicPrefetch are enabled, and reschedules the prefetch with PrefetchPriority.Intent.
Sources: packages/next/src/client/components/links.ts:121-141, packages/next/src/client/components/links.ts:168-194, packages/next/src/client/components/links.ts:249-300Note
Prefetching on viewport intersection is explicitly disabled in development environments (NODE_ENV !== 'production') for performance reasons to avoid compiling target pages prematurely during local inspection.
Sources: packages/next/src/client/components/links.ts:260-265
When visible links must be refreshed due to changes in nextUrl, the root route tree, or cache invalidations, pingVisibleLinks() iterates over prefetchableAndVisible. If isPrefetchTaskDirty() returns true, it cancels the existing task via cancelPrefetchTask() and schedules a new one.
Sources: packages/next/src/client/components/links.ts:354-386
Beyond automatic link observation, the public prefetch function serves as the direct entrypoint for imperative prefetching through router methods or custom link wrappers. It validates the target URL via createPrefetchURL(), constructs a cache key incorporating any interception route nextUrl, and delegates task creation to the scheduler.
Sources: packages/next/src/client/components/segment-cache/prefetch.ts:27-47
Sources: packages/next/src/client/components/links.ts:168-232, packages/next/src/client/components/links.ts:259-300, packages/next/src/client/components/links.ts:354-386, packages/next/src/client/components/segment-cache/prefetch.ts:27-47
The segment cache manages the storage, lifecycle states, and retention of prefetched React Server Component (RSC) route trees and individual route segments. Stored entries transition through defined status stages while tracking dynamic staleness and vary-path parameters to prevent data races during parallel prefetches. Sources: packages/next/src/client/components/segment-cache/scheduler.ts:710-726, packages/next/src/client/components/segment-cache/cache.ts:2747-2863
When a prefetch or runtime request writes server responses into the cache, entries move from uninitialized states to fulfilled or rejected cache records. The function call chain governing runtime entry fulfillment and storage proceeds through:
writeSeedDataIntoCache() → fulfillEntrySpawnedByRuntimePrefetch() → fulfillSegmentCacheEntry() → setInCacheMap() or upsertSegmentEntry()
Sources: packages/next/src/client/components/segment-cache/cache.ts:2686-2863
During this flow, writeSeedDataIntoCache recursively unpacks seed data slots. fulfillEntrySpawnedByRuntimePrefetch determines whether to re-key the entry under a more generic vary path using getFulfilledSegmentVaryPath or tree.shellVaryPath. It checks if an entry is owned by the current task via entriesOwnedByCurrentTask.get(tree.requestKey). If owned, it fulfills the existing entry; otherwise, it creates a detached entry or upserts it into the global segmentCacheMap.
Sources: packages/next/src/client/components/segment-cache/cache.ts:2686-2863
Warning
Never write directly over an entry created by a different task without checking task ownership; doing so introduces data races across concurrent prefetch streams. Sources: packages/next/src/client/components/segment-cache/cache.ts:2796-2802
Entries maintain expiration timestamps (staleAt) calculated from server-sent headers or async iterables. getStaleAt evaluates an optional staleTimeIterable by iterating through yielded values and taking the final timestamp, falling back to getStaleAtFromHeader or a default static staleness window (STATIC_STALETIME_MS). For route tree misses where requests take longer than a minute, a temporary staleAt of now + 60 * 1000 is assigned so that subsequent requests retry instead of blocking indefinitely.
Sources: packages/next/src/client/components/segment-cache/scheduler.ts:702-709, packages/next/src/client/components/segment-cache/cache.ts:3070-3109
Sources: packages/next/src/client/components/segment-cache/scheduler.ts:684-731, packages/next/src/client/components/segment-cache/cache.ts:3060-3109
Optimistic Routing enables the client to predict route structures for URLs that have not yet been prefetched by leveraging previously learned route patterns. Stored in a trie indexed by URL path segments (KnownRoutePart), these patterns map URL structures to route templates. When a user navigates to a URL with no direct prefetch cache entry, the client matches the candidate URL against the known route tree to synthesize a route entry instantly, avoiding a prefetch round-trip.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:1-44
When the server returns a route tree during an initial load, navigation, or prefetch, the client calls discoverKnownRoute(). This function parses the pathname into segments and invokes discoverKnownRoutePart(), which walks the route tree and URL parts in parallel to populate the trie.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:199-272, packages/next/src/client/components/segment-cache/optimistic-routes.ts:345-362
The call-chain execution walkthrough for discovering and caching a known route proceeds through:
discoverKnownRoute() → fulfillRouteCacheEntry() → discoverKnownRoutePart() → writeRouteIntoCache() or readPattern()
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:199-272
During this recursion, discoverKnownRoutePart evaluates whether a segment is static or dynamic, records static siblings into staticChildren, and caches the resulting route template in knownRoutePart.pattern.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:370-599
Warning
If a static segment or dynamic boundary in the URL does not match the route tree structure, discovery immediately aborts trie population via handleMismatchDueToRewrite(), preventing malformed pattern predictions while still writing the valid entry into the standard cache.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:275-306, packages/next/src/client/components/segment-cache/optimistic-routes.ts:376-388
When looking up an uncached route, matchKnownRoute() splits the pathname and invokes matchKnownRoutePart(). Matching prioritizes static child nodes before evaluating dynamic children ([param], [...param], ...param), collecting parameter values in a ResolvedParams map.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:607-621, packages/next/src/client/components/segment-cache/optimistic-routes.ts:716-777
Once a matching pattern is found, reifyRouteTree() clones the template route tree, substituting the resolved parameter values into dynamic segments and recomputing vary paths to generate a concrete synthetic entry (FulfilledRouteCacheEntry).
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:648-693, packages/next/src/client/components/segment-cache/optimistic-routes.ts:879-982
Note
The trie distinguishes between a null and an empty Map for staticChildren: null indicates that static siblings are completely unknown (such as in webpack development mode on-demand compilation), forcing the matcher to deopt to server resolution rather than risk false-positive dynamic matches.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:98-106, packages/next/src/client/components/segment-cache/optimistic-routes.ts:732-741
Optimistic routing incorporates protection against dynamic rewrites and path mismatches. If the server returns a response whose pathname diverges from what was predicted, the route entry is marked with hasDynamicRewrite = true.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:227-230, packages/next/src/client/components/segment-cache/optimistic-routes.ts:564-567
When matchKnownRoute encounters a pattern where hasDynamicRewrite is true, or where couldBeIntercepted is set, it rejects the prediction and returns null, forcing the router to fall back to standard server resolution.
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:641-643, packages/next/src/client/components/segment-cache/optimistic-routes.ts:735-738
Sources: packages/next/src/client/components/segment-cache/optimistic-routes.ts:33-44, packages/next/src/client/components/segment-cache/optimistic-routes.ts:745-748, packages/next/src/client/components/segment-cache/optimistic-routes.ts:879-885
Partial Prerendering (PPR) navigation execution bridges client-side cache traversal and server-driven dynamic updates. When a user navigates to a new location via navigate(), the router checks the route segment cache for a fulfilled entry. If a matching route tree is found, it immediately builds a copy-on-write NavigationTask and patches the app router state, allowing the static prefetch shell to render instantly while any missing dynamic data is deferred.
Sources: packages/next/src/client/components/segment-cache/navigation.ts:58-148, packages/next/src/client/components/router-reducer/ppr-navigations.ts:163-190
The navigation and reconciliation pipeline flows through a series of deterministic functions that transition raw URL requests into updated router trees and dynamic server fetches:
navigate() → navigateImpl() → navigateUsingPrefetchedRouteTree() → navigateToKnownRoute() → startPPRNavigation() → updateCacheNodeOnNavigation()
Sources: packages/next/src/client/components/segment-cache/navigation.ts:58-387, packages/next/src/client/components/router-reducer/ppr-navigations.ts:190-228
navigate() acts as the entry point, coordinating testing locks and calling navigateImpl().
Sources: packages/next/src/client/components/segment-cache/navigation.ts:58-112navigateImpl() queries the route cache using readRouteCacheEntry(). If fulfilled, it invokes navigateUsingPrefetchedRouteTree().
Sources: packages/next/src/client/components/segment-cache/navigation.ts:114-149navigateUsingPrefetchedRouteTree() extracts the target RouteTree and delegates to navigateToKnownRoute().
Sources: packages/next/src/client/components/segment-cache/navigation.ts:360-387navigateToKnownRoute() sets up a NavigationRequestAccumulation context and invokes startPPRNavigation().
Sources: packages/next/src/client/components/segment-cache/navigation.ts:214-328startPPRNavigation() wraps the root refresh state and calls updateCacheNodeOnNavigation().
Sources: packages/next/src/client/components/router-reducer/ppr-navigations.ts:190-228updateCacheNodeOnNavigation() compares the new route segments against the old FlightRouterState, determining whether to reuse cached nodes or switch to createCacheNodeOnNavigation() for divergent subtrees.
Sources: packages/next/src/client/components/router-reducer/ppr-navigations.ts:230-256Note
If startPPRNavigation() returns null, indicating that no SPA-compatible transitions could be resolved, the router falls back to completeHardNavigation(), executing a traditional full-page MPA reload.
Sources: packages/next/src/client/components/segment-cache/navigation.ts:356-358
Navigation tasks and freshness rules dictate how cache entries and server requests interact during routing operations.
Warning
During gesture navigations (FreshnessPolicy.Gesture), dynamic request spawning is deliberately suppressed by navigateToKnownRoute() to avoid wasteful server invocations on mere hover events before an actual click occurs.
Sources: packages/next/src/client/components/segment-cache/navigation.ts:330-341
During static generation with Partial Prerendering (PPR) enabled (experimental.isRoutePPREnabled), Next.js manages server prerendering through a staged process that isolates dynamic holes, tracks dynamic access patterns, and produces static Flight streams.
Sources: packages/next/src/server/app-render/app-render.tsx:8052-8056
The server prerendering sequence coordinates dynamic tracking stores, RSC payload generation, React Server streaming, and HTML prelude processing:
createDynamicTrackingState() initializes dynamic tracking stores based on debug options.
Sources: packages/next/src/server/app-render/app-render.tsx:8054-8054workUnitAsyncStorage.run() binds the pprReactServerPrerenderStore context to execute getRSCPayload().
Sources: packages/next/src/server/app-render/app-render.tsx:8070-8076createReactServerPrerenderResultFromRender() wraps the result of renderFlightStream(), producing the unclosing server stream.
Sources: packages/next/src/server/app-render/app-render.tsx:8079-8091getClientPrerender() renders the <App /> tree using the PPR prerender store, returning an unprocessedPrelude and postponed state.
Sources: packages/next/src/server/app-render/app-render.tsx:8107-8127streamToBuffer() reads the full React Server render stream into flightData, which is then passed to collectSegmentData() if shouldGenerateStaticFlightData() evaluates to true.
Sources: packages/next/src/server/app-render/app-render.tsx:8139-8151processPreludeOp() processes the unprocessed prelude to yield the final prelude and preludeIsEmpty status.
Sources: packages/next/src/server/app-render/app-render.tsx:8153-8154Note
Awaiting the complete RSC render stream via streamToBuffer(reactServerResult.asStream()) guarantees that dynamic API usages anywhere within the Server Component tree are captured—even if those specific branches are omitted from the initial SSR HTML prelude.
Sources: packages/next/src/server/app-render/app-render.tsx:8136-8140
When prerendering completes, Next.js inspects the dynamic access tracking to categorize the output into one of three distinct outcomes: Dynamic HTML, Dynamic Data, or fully Static. Sources: packages/next/src/server/app-render/app-render.tsx:8156-8171
Warning
If a prerender has dynamic holes (Dynamic HTML), the engine skips embedding server-inserted HTML and inlined Flight data into the static output, requiring runtime resumption when client requests arrive.
Sources: packages/next/src/server/app-render/app-render.tsx:8159-8163
The back-forward cache (bfcache) integrates with client-side routing to persist session history state, coordinate dynamic segment upgrades, and manage the cache restore lifecycle across history traversals and regular navigations. It maintains a separate memory store (bfcacheMap) using the CacheMap data structure, tracking BFCacheEntry records containing rendered server components (rsc), prefetched server components (prefetchRsc), page metadata (head), prefetched metadata (prefetchHead), persistent bfcacheId values, and staleness parameters.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:32-60
The back-forward cache exposes several exported functions to write, read, and invalidate persisted entry states depending on navigation type and staleness conditions:
invalidateBfCache() increments currentBfCacheVersion, invalidating existing back-forward cache entries when called in a browser environment.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:63-68writeToBFCache() constructs a BFCacheEntry with status: EntryStatus.Fulfilled and stores it under the provided varyPath.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:70-114writeHeadToBFCache() delegates head-data writing directly to writeToBFCache().
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:116-135updateBFCacheEntryStaleAt() retrieves an entry bypassing staleness checks using -1 and updates its staleAt property with a per-page value from unstable_dynamicStaleTime.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:137-163readFromBFCache() queries bfcacheMap passing -1 as the timestamp to bypass staleness evaluation during back-forward history traversals.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:165-183readFromBFCacheDuringRegularNavigation() evaluates entries against the real now timestamp during standard navigations.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:185-201Note
During a back-forward navigation, readFromBFCache passes -1 instead of the current timestamp to getFromCacheMap, explicitly bypassing staleness checks so cached session history state is always restored regardless of age.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:171-183
Dynamic stale times received via the Flight response d field are normalized into absolute timestamps via computeDynamicStaleAt(), falling back to DYNAMIC_STALETIME_MS when UnknownDigitalStaleTime (-1) is supplied.
Sources: packages/next/src/client/components/segment-cache/bfcache.ts:5-22
The Instant Navigation Testing API manages synchronization via an in-memory lock (NavigationLockState) and a persistent cookie (NEXT_INSTANT_TEST_COOKIE). When an external testing harness or devtools initiates a capture scope, it creates a pending cookie state. Next.js reads this state, acquires the lock, and intercepts outgoing client fetches.
The lock lifecycle transitions through exact internal functions: startListeningForInstantNavigationCookie() inspects initial state and attaches listeners, calling acquireLock() to instantiate a promise and override window.fetch with globalFetchOverride, and invoking releaseLock() alongside refreshOnInstantNavigationUnlock() when the test cookie is deleted.
Sources: packages/next/src/client/components/segment-cache/navigation-testing-lock.ts:87-118, packages/next/src/client/components/segment-cache/navigation-testing-lock.ts:158-216
Warning
globalFetchOverride pins execution to the pre-lock window.fetch captured during acquireLock(). If a user-installed fetch override is attached after the lock scope begins, it remains bypassed until the navigation lock is fully released and the original fetch reference is restored.
Sources: packages/next/src/client/components/segment-cache/navigation-testing-lock.ts:131-150
The development debug channel streams diagnostic chunks for requests identified by NEXT_REQUEST_ID_HEADER or self.__next_r. Initial document debug streams are buffered using a TransformStream, written asynchronously to IndexedDB (__next_debug_channel database under the channels store) during idle periods (whenIdle()), and pruned to maintain a maximum bound of 10 entries using the createdAt index.
When a cached HTML document is restored from the browser cache, createDebugChannel() evaluates navigation timing metrics via wasServedFromCacheKnownAtExec() and wasServedFromCacheAtPageshow(). If chunks are missing from IndexedDB during a cache restore, restoreDebugChannelOrReload() triggers an unconditional location.reload() while parking the stream to prevent hydration errors.
Sources: packages/next/src/client/dev/debug-channel.ts:200-285, packages/next/src/client/dev/debug-channel.ts:361-432
Note
wasServedFromCacheKnownAtExec() checks Safari's tab-duplication signature (type === 'navigate', responseStart === 0, responseEnd > 0) alongside standard transferSize and encodedBodySize metrics to distinguish HTTP cache restorations from fresh server fetches prior to the pageshow event.
Sources: packages/next/src/client/dev/debug-channel.ts:200-251