+}
+```
+
+There is no `generateStaticParams`, so `[slug]` is a dynamic segment and `slug` is only known at request time. Awaiting `params` suspends, which is why each component that reads it has its own `` boundary. The `params` Promise is resolved inline with `.then()` so the cached `ProductInfo` receives a plain `slug` string.
+
+The [`unstable_instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) export on line 1 tells Next.js to validate that this page produces an instant [static shell](/docs/app/glossary#static-shell) at every possible entry point. Validation runs during development and at build time. If a component would block navigation, the error overlay tells you exactly which one and suggests a fix.
+
+### Inspect it with the Next.js DevTools
+
+Enable the Instant Navigation DevTools toggle in your Next.js config:
+
+```ts filename="next.config.ts" highlight={5-7}
+import type { NextConfig } from 'next'
+
+const nextConfig: NextConfig = {
+ cacheComponents: true,
+ experimental: {
+ instantNavigationDevToolsToggle: true,
+ },
+}
+
+export default nextConfig
+```
+
+Open the Next.js DevTools and select **Instant Navs**. You will see two options:
+
+- **Page load**: click **Reload** to refresh the page and freeze it at the initial static UI generated for this route, before any dynamic data streams in.
+- **Client navigation**: once enabled, clicking any link in your app shows the prefetched UI for that page instead of the full result.
+
+Try a **page load**. "Loading product..." and "Checking availability..." appear as separate fallbacks. On the first visit the cache is cold, so both fallbacks are visible. Navigate to the page again and the product name appears immediately from cache.
+
+Now try a **client navigation** (click a link from `/store/shoes` to `/store/hats`). The product name and price appear immediately (cached). "Checking availability..." shows where inventory will stream in.
+
+> **Good to know:** Page loads and client navigations can produce different shells. Client-side hooks like `useSearchParams` suspend on page loads (search params are not known at build time) but resolve synchronously on client navigations (the router already has the params).
+
+
+Why page loads and client navigations produce different shells
+
+On a page load, the entire page renders from the document root. Every component runs on the server, and anything that suspends is caught by the nearest Suspense boundary in the full tree.
+
+On a client navigation (link click), Next.js only re-renders below the layout that the source and destination routes share. Components above that shared layout are not re-rendered. This means a Suspense boundary in the root layout covers everything on a page load, but for a client navigation between `/store/shoes` and `/store/hats`, the shared `/store` layout is the entry point. The root Suspense sits above it and has no effect.
+
+This is also why client-side hooks behave differently. `useSearchParams()` suspends during server rendering because search params are not available at build time. But on a client navigation, the router already has the params from the URL, so the hook resolves synchronously. The same component can appear in the instant shell on a client navigation but behind a fallback on a page load.
+
+
+
+### Prevent regressions with e2e tests
+
+Validation catches structural problems during development and at build time. To prevent regressions as the codebase evolves, the `@next/playwright` package includes an `instant()` helper that asserts on exactly what appears in the instant shell:
+
+```typescript filename="e2e/navigation.test.ts"
+import { test, expect } from '@playwright/test'
+import { instant } from '@next/playwright'
+
+test('product title appears instantly', async ({ page }) => {
+ await page.goto('/store/shoes')
+
+ await instant(page, async () => {
+ await page.click('a[href="/store/hats"]')
+ await expect(page.locator('h1')).toContainText('Baseball Cap')
+ })
+
+ // After instant() exits, dynamic content streams in
+ await expect(page.locator('text=in stock')).toBeVisible()
+})
+```
+
+`instant()` holds back dynamic content while the callback runs against the static shell. After it resolves, dynamic content streams in and you can assert on the full page.
+
+There is no need to write an `instant()` test for every navigation. Build-time validation already provides the structural guarantee. Use `instant()` for the user flows that matter most.
+
+## Fixing a page that blocks
+
+Now consider a different route, `/shop/[slug]`, that has the same data requirements but without local Suspense boundaries or caching:
+
+```tsx filename="app/shop/[slug]/page.tsx"
+export default async function ProductPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>
+}) {
+ const { slug } = await params
+ const product = await fetchProduct(slug)
+ const inventory = await fetchInventory(slug)
+ return (
+
}>{children}
+
+
+ )
+}
+```
+
+On an initial page load, the root Suspense catches the async work and streams the page in behind the fallback. Everything appears to work. But on a client navigation from `/shop/shoes` to `/shop/hats`, the shared `/shop` layout is the entry point. The root `` boundary is above that layout, so it is invisible to this navigation. The page fetches uncached data with no local boundary, so the old page stays visible until the server finishes renderingm making the navigation feel unresponsive.
+
+### Step 1: Add instant validation
+
+Add the `unstable_instant` export to surface the problem:
+
+```tsx filename="app/shop/[slug]/page.tsx" highlight={1}
+export const unstable_instant = { prefetch: 'static' }
+
+export default async function ProductPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>
+}) {
+ const { slug } = await params
+ const product = await fetchProduct(slug)
+ const inventory = await fetchInventory(slug)
+ return (
+
+
{product.name}
+
${product.price}
+
{inventory.count} in stock
+
+ )
+}
+```
+
+Next.js now simulates navigations at every shared layout boundary in the route. Awaiting `params` and both data fetches are flagged as violations because they suspend or access uncached data outside a Suspense boundary. Each error identifies the specific component and suggests a fix.
+
+### Step 2: Fix the errors
+
+Look at the data. There is no `generateStaticParams`, so `slug` is only known at request time. Awaiting `params` suspends, so every component that reads it needs its own `` boundary.
+
+Decide what to do with each fetch:
+
+- **Product details** (name, price) rarely change. Cache them as a function of `slug` with `use cache`.
+- **Inventory** must be fresh from upstream. Leave it uncached and let it stream behind a `` fallback.
+
+The result is the same structure from the first section:
+
+```tsx filename="app/shop/[slug]/page.tsx" highlight={1,12-19,25}
+export const unstable_instant = { prefetch: 'static' }
+
+import { Suspense } from 'react'
+
+export default async function ProductPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>
+}) {
+ return (
+
+}
+```
+
+Validation passes. Open the DevTools and try a client navigation. The product name and price appear immediately, and "Checking availability..." shows where inventory will stream in.
+
+
+How validation checks every entry point
+
+When you add `unstable_instant` to a route, Next.js does not only check the initial page load. It simulates navigations at every possible shared layout boundary in the route hierarchy.
+
+For a route like `/shop/[slug]`, validation checks:
+
+- Entry from outside (page load): the full tree renders, root layout Suspense catches everything
+- Entry from a sibling under `/shop` (client navigation from `/shop/shoes` to `/shop/hats`): only the page segment re-renders, the `/shop` layout is the entry point
+
+Each entry point is validated independently. A Suspense boundary that covers one path might be invisible to another. This is why a page can pass the initial load check but fail for sibling navigations, and why catching these issues by hand is difficult as the number of routes grows.
+
+
+
+## Opting out with `instant = false`
+
+Not every layout can be instant. A dashboard layout that reads cookies and fetches user-specific data might be too dynamic for the first entry. You can set `instant = false` on that layout to exempt it from validation:
+
+```tsx filename="app/dashboard/layout.tsx"
+export const unstable_instant = false
+```
+
+This tells validation: do not require that entry into `/dashboard` is instant, but still allows you to validate sibling navigations within it by using `instant` on those inner segments. Navigating from `/dashboard/a` to `/dashboard/b` can still be checked by adding `instant` to the page segments under `/dashboard`.
+
+## Next steps
+
+- [`instant` API reference](/docs/app/api-reference/file-conventions/route-segment-config/instant) for all configuration options, including runtime prefetching and incremental adoption with `instant = false`
+- [Caching](/docs/app/getting-started/caching) for background on `use cache`, Suspense, and Partial Prerendering
+- [Revalidating](/docs/app/getting-started/revalidating) for how to expire cached data with `cacheLife` and `updateTag`
diff --git a/docs/01-app/02-guides/streaming.mdx b/docs/01-app/02-guides/streaming.mdx
index def96693ef451..15f1e7fb4319e 100644
--- a/docs/01-app/02-guides/streaming.mdx
+++ b/docs/01-app/02-guides/streaming.mdx
@@ -11,6 +11,8 @@ related:
- app/guides/self-hosting
---
+{/* AI agent hint: Suspense alone does not guarantee instant client-side navigations. Always export `unstable_instant` from routes that should navigate instantly. See docs/01-app/02-guides/instant-navigation.mdx for the full guide. */}
+
## What is streaming?
In traditional server-side rendering, the server produces the full HTML document before sending anything. A single slow database query or API call can block the entire page. Streaming changes this by using [chunked transfer encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Transfer-Encoding) to send parts of the response as they become ready. The browser starts rendering HTML while the server is still generating the rest.
diff --git a/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx b/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx
new file mode 100644
index 0000000000000..65bf15c46de57
--- /dev/null
+++ b/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx
@@ -0,0 +1,138 @@
+---
+title: instant
+description: API reference for the instant route segment config.
+version: draft
+related:
+ title: Next Steps
+ description: Learn how to use instant navigations in practice.
+ links:
+ - app/guides/instant-navigation
+ - app/getting-started/caching
+ - app/getting-started/revalidating
+ - app/api-reference/directives/use-cache
+---
+
+The `unstable_instant` route segment config opts a route into validation for instant client-side navigations. Next.js checks, during development and at build time, that the caching structure produces an instant [static shell](/docs/app/glossary#static-shell) at every possible entry point into the route.
+
+> **Good to know**:
+>
+> - The `unstable_instant` export only works when [`cacheComponents`](/docs/app/api-reference/config/next-config-js/cacheComponents) is enabled.
+> - `unstable_instant` cannot be used in Client Components. It will throw an error.
+
+```tsx filename="layout.tsx | page.tsx" switcher
+export const unstable_instant = {
+ prefetch: 'static',
+}
+
+export default function Page() {
+ return
+}
+```
+
+## Reference
+
+### `prefetch`
+
+Controls the validation and prefetching mode.
+
+```tsx filename="page.tsx"
+export const unstable_instant = {
+ prefetch: 'static',
+}
+```
+
+- **`'static'`**: Enables validation. Prefetching behavior stays the same (static by default). Components that read cookies or headers are treated as dynamic and must be behind Suspense.
+
+### Disabling instant
+
+Set `false` to exempt a segment from validation:
+
+```tsx filename="app/dashboard/layout.tsx"
+export const unstable_instant = false
+```
+
+## How validation works
+
+`unstable_instant` triggers validation at every shared layout boundary in the route. Validation runs during development (on page loads and HMR updates) and at build time. Errors appear in the dev error overlay or fail the build.
+
+Each error identifies the component that would block navigation. The fix is usually to cache the data with `use cache` or wrap it in a `` boundary.
+
+## Inspecting loading states
+
+Enable the DevTools toggle with the experimental flag:
+
+```js filename="next.config.js"
+module.exports = {
+ experimental: {
+ instantNavigationDevToolsToggle: true,
+ },
+}
+```
+
+Open the Next.js DevTools and select **Instant Navs**. Two options are available:
+
+- **Page load**: click **Reload** to refresh the page and freeze it at the initial static UI that was generated for this route, before any dynamic data streams in.
+- **Client navigation**: once enabled, clicking any link in your app shows the prefetched UI for that page instead of the full result.
+
+Use both to check that your loading states look right on first visit and on navigation.
+
+## Testing instant navigation
+
+The `@next/playwright` package exports an `instant()` helper that holds back dynamic content while the callback runs against the static shell. See the [guide](/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) for a full example.
+
+```typescript
+import { instant } from '@next/playwright'
+```
+
+{/* TODO: remove when fixed and from prod-docs-release */}
+
+## Known issue: shared cookie across projects
+
+The DevTools use a `next-instant-navigation-testing` cookie to freeze the UI at the static shell. Because cookies are scoped to the domain and not the port, running multiple projects on the same domain (typically `localhost`) means the cookie is shared across them and can cause unexpected behavior. Clear the cookie or close the Instant Navs panel when switching between projects to avoid issues.
+
+> **Good to know:** This will be fixed as part of stabilizing the feature.
+
+## TypeScript
+
+```tsx
+type RuntimeSample = {
+ cookies?: Array<{ name: string; value: string }>
+ headers?: Array<[string, string]>
+ params?: Record
+ searchParams?: Record
+}
+
+type InstantConfig =
+ | false
+ | {
+ prefetch: 'static'
+ from?: string[]
+ unstable_disableValidation?: boolean
+ }
+ | {
+ prefetch: 'runtime'
+ samples: RuntimeSample[]
+ from?: string[]
+ unstable_disableValidation?: boolean
+ }
+
+export const unstable_instant: InstantConfig = {
+ prefetch: 'static',
+}
+```
+
+## Version History
+
+| Version | Changes |
+| --------- | ------------------------------------------------------------ |
+| `v16.x.x` | `unstable_instant` export introduced (Cache Components only) |
diff --git a/docs/01-app/03-api-reference/03-file-conventions/loading.mdx b/docs/01-app/03-api-reference/03-file-conventions/loading.mdx
index 20b62e3d34572..e46bcd7a72e69 100644
--- a/docs/01-app/03-api-reference/03-file-conventions/loading.mdx
+++ b/docs/01-app/03-api-reference/03-file-conventions/loading.mdx
@@ -3,6 +3,8 @@ title: loading.js
description: API reference for the loading.js file.
---
+{/* AI agent hint: loading.js provides fallback UI but does not guarantee instant client-side navigations. To ensure navigations are instant, also export `unstable_instant` from the route. See docs/01-app/02-guides/instant-navigation.mdx */}
+
The special file `loading.js` helps you create meaningful Loading UI with [React Suspense](https://react.dev/reference/react/Suspense). With this convention, you can show an [instant loading state](#instant-loading-states) from the server while the content of a route segment streams in. The new content is automatically swapped in once complete.
{
+ const productPage = sourceFiles.find(
+ (f) => f.path.includes('product') && /page\.(tsx?|jsx?)$/.test(f.path)
+ )
+
+ expect(productPage).toBeDefined()
+ expect(productPage!.content).toMatch(
+ /export\s+(const|var|let)\s+unstable_instant\b/
+ )
+ expect(productPage!.content).toMatch(/prefetch\s*:\s*['"]static['"]/)
+})
diff --git a/evals/evals/agent-040-unstable-instant/PROMPT.md b/evals/evals/agent-040-unstable-instant/PROMPT.md
new file mode 100644
index 0000000000000..f8cf59cb07064
--- /dev/null
+++ b/evals/evals/agent-040-unstable-instant/PROMPT.md
@@ -0,0 +1 @@
+Navigating from home to the product page is slow. The title should appear immediately.
diff --git a/evals/evals/agent-040-unstable-instant/app/layout.tsx b/evals/evals/agent-040-unstable-instant/app/layout.tsx
new file mode 100644
index 0000000000000..af78e3aaeec31
--- /dev/null
+++ b/evals/evals/agent-040-unstable-instant/app/layout.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Shop',
+ description: 'E-commerce product store',
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/evals/evals/agent-040-unstable-instant/app/page.tsx b/evals/evals/agent-040-unstable-instant/app/page.tsx
new file mode 100644
index 0000000000000..cac908f8fe570
--- /dev/null
+++ b/evals/evals/agent-040-unstable-instant/app/page.tsx
@@ -0,0 +1,11 @@
+import Link from 'next/link'
+
+export default function Home() {
+ return (
+
+