Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: prefetchInlining
description: Override how the App Router bundles small prefetch responses together.
version: experimental
related:
title: Related
description: View related API references and guides.
links:
- app/api-reference/components/link
- app/guides/prefetching
---

When the App Router prefetches a route, it can bundle small segment responses into a single response instead of requesting each one separately. This reduces the number of prefetch requests at the cost of duplicating some shared segment data across routes. This behavior is on by default, and most apps should leave it that way.

The `experimental.prefetchInlining` option lets you override this behavior or disable inlining while debugging navigation issues or measuring request volume. For most applications, there is no need to change the default behavior.

> **Good to know**: The inlining behavior is a permanent part of the App Router. Only the `experimental.prefetchInlining` configuration is experimental, so its options may still change.

## Usage

To turn off prefetch inlining, set `experimental.prefetchInlining` to `false`:

```ts filename="next.config.ts" switcher
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
experimental: {
prefetchInlining: false,
},
}

export default nextConfig
```

```js filename="next.config.js" switcher
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
prefetchInlining: false,
},
}

module.exports = nextConfig
```

To override the thresholds instead of disabling inlining, pass an object. Any value you omit keeps its default:

```ts filename="next.config.ts" switcher
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
experimental: {
prefetchInlining: {
maxSize: 2048,
maxBundleSize: 10240,
},
},
}

export default nextConfig
```

```js filename="next.config.js" switcher
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
prefetchInlining: {
maxSize: 2048,
maxBundleSize: 10240,
},
},
}

module.exports = nextConfig
```

## Reference

| Value | Description |
| -------- | ---------------------------------------------------------------------------- |
| `true` | Inlines prefetch responses with the default thresholds. This is the default. |
| `false` | Disables prefetch inlining. Each segment is prefetched as its own request. |
| `object` | Inlines prefetch responses using the `maxSize` or `maxBundleSize` you set. |

When you pass an object, the following options control the thresholds. Both are measured in bytes of the gzip-compressed segment response:

| Option | Type | Default | Description |
| --------------- | -------- | ------- | --------------------------------------------------------------------------------------- |
| `maxSize` | `number` | `2048` | Largest a single segment response can be to still be eligible for inlining. |
| `maxBundleSize` | `number` | `10240` | Largest total size that can be inlined into one bundled prefetch response along a path. |

Lower thresholds keep more per-segment deduplication; higher thresholds inline more data and cut request count further.

## Version History

| Version | Change |
| ------- | --------------------------------------------------- |
| 16.3.0 | `experimental.prefetchInlining` enabled by default. |
| 16.2.0 | `experimental.prefetchInlining` added. |
2 changes: 2 additions & 0 deletions packages/create-next-app/helpers/generate-agent-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export function generateAgentFiles(root: string): void {
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in \`node_modules/next/dist/docs/\` before writing any code. Heed deprecation notices.

**Keep this block, including in commits.** It is part of the project's agent setup, maintained by \`next dev\` for every agent that works here. If it appears as an uncommitted change, that is intentional — commit it as-is. Do not remove it to clean up a diff; it will be regenerated.
<!-- END:nextjs-agent-rules -->
`

Expand Down
50 changes: 36 additions & 14 deletions packages/next-codemod/bin/agents-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import pc from 'picocolors'
import { BadInput } from './shared'
import {
getNextjsVersion,
getBundledDocsInfo,
getBundledDocsLinkPath,
pullDocs,
collectDocFiles,
buildDocTree,
Expand Down Expand Up @@ -74,8 +76,20 @@ export async function runAgentsMd(options: AgentsMdOptions): Promise<void> {
}

const claudeMdPath = path.join(cwd, targetFile)
const docsPath = path.join(cwd, DOCS_DIR_NAME)
const docsLinkPath = `./${DOCS_DIR_NAME}`

// Next.js >= 16.2.0 ships its docs inside the published package. When the
// installed version matches the requested one, index the bundled docs
// directly instead of downloading a copy into .next-docs.
const bundledDocs = getBundledDocsInfo(cwd)
const useBundledDocs =
bundledDocs !== null && bundledDocs.version === nextjsVersion

const docsPath = useBundledDocs
? bundledDocs.docsPath
: path.join(cwd, DOCS_DIR_NAME)
const docsLinkPath = useBundledDocs
? getBundledDocsLinkPath(cwd, bundledDocs.docsPath)
: `./${DOCS_DIR_NAME}`

let sizeBefore = 0
let isNewFile = true
Expand All @@ -87,18 +101,24 @@ export async function runAgentsMd(options: AgentsMdOptions): Promise<void> {
isNewFile = false
}

console.log(
`\nDownloading Next.js ${pc.cyan(nextjsVersion)} documentation to ${pc.cyan(DOCS_DIR_NAME)}...`
)
if (useBundledDocs) {
console.log(
`\nUsing the docs bundled with Next.js ${pc.cyan(nextjsVersion)} at ${pc.cyan(docsLinkPath)} (no download needed).`
)
} else {
console.log(
`\nDownloading Next.js ${pc.cyan(nextjsVersion)} documentation to ${pc.cyan(DOCS_DIR_NAME)}...`
)

const pullResult = await pullDocs({
cwd,
version: nextjsVersion,
docsDir: docsPath,
})
const pullResult = await pullDocs({
cwd,
version: nextjsVersion,
docsDir: docsPath,
})

if (!pullResult.success) {
throw new BadInput(`Failed to pull docs: ${pullResult.error}`)
if (!pullResult.success) {
throw new BadInput(`Failed to pull docs: ${pullResult.error}`)
}
}

const docFiles = collectDocFiles(docsPath)
Expand All @@ -115,15 +135,17 @@ export async function runAgentsMd(options: AgentsMdOptions): Promise<void> {

const sizeAfter = Buffer.byteLength(newContent, 'utf-8')

const gitignoreResult = ensureGitignoreEntry(cwd)
// .next-docs only exists on the download path; bundled docs live in
// node_modules, which is already ignored.
const gitignoreResult = useBundledDocs ? null : ensureGitignoreEntry(cwd)

const action = isNewFile ? 'Created' : 'Updated'
const sizeInfo = isNewFile
? formatSize(sizeAfter)
: `${formatSize(sizeBefore)} → ${formatSize(sizeAfter)}`

console.log(`${pc.green('✓')} ${action} ${pc.bold(targetFile)} (${sizeInfo})`)
if (gitignoreResult.updated) {
if (gitignoreResult?.updated) {
console.log(
`${pc.green('✓')} Added ${pc.bold(DOCS_DIR_NAME)} to .gitignore`
)
Expand Down
117 changes: 117 additions & 0 deletions packages/next-codemod/lib/__tests__/agents-md-e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,123 @@ This is my project documentation.
}
}, 30000) // Increase timeout for git clone

describe('bundled docs (Next.js >= 16.2.0)', () => {
// Simulate an install of a Next.js version that ships docs inside the
// published package at node_modules/next/dist/docs.
function setupBundledNext(projectDir, version) {
const nextDir = path.join(projectDir, 'node_modules', 'next')
const gettingStartedDir = path.join(
nextDir,
'dist',
'docs',
'01-app',
'01-getting-started'
)
fs.mkdirSync(gettingStartedDir, { recursive: true })
fs.writeFileSync(
path.join(nextDir, 'package.json'),
JSON.stringify({ name: 'next', version })
)
fs.writeFileSync(
path.join(nextDir, 'dist', 'docs', 'index.md'),
'# Next.js Docs'
)
fs.writeFileSync(
path.join(gettingStartedDir, '01-installation.md'),
'# Installation'
)
fs.writeFileSync(
path.join(gettingStartedDir, '02-project-structure.md'),
'# Project Structure'
)
}

it('indexes bundled docs instead of downloading when installed Next.js ships them', async () => {
setupBundledNext(testProjectDir, '16.2.0')

const originalCwd = process.cwd()
process.chdir(testProjectDir)

try {
await runAgentsMd({ output: 'CLAUDE.md' })

// No .next-docs copy and no .gitignore entry for it
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
false
)
expect(fs.existsSync(path.join(testProjectDir, '.gitignore'))).toBe(
false
)

const claudeMdContent = fs.readFileSync(
path.join(testProjectDir, 'CLAUDE.md'),
'utf-8'
)
expect(claudeMdContent).toContain(
'root: ./node_modules/next/dist/docs'
)
expect(claudeMdContent).toContain('01-installation.md')

const output = consoleOutput.join('\n')
expect(output).toContain('bundled with Next.js')
expect(output).not.toContain('Downloading')
} finally {
process.chdir(originalCwd)
}
})

it('uses bundled docs when --version matches the installed version', async () => {
setupBundledNext(testProjectDir, '16.2.0')

const originalCwd = process.cwd()
process.chdir(testProjectDir)

try {
await runAgentsMd({ version: '16.2.0', output: 'AGENTS.md' })

expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
false
)

const agentsMdContent = fs.readFileSync(
path.join(testProjectDir, 'AGENTS.md'),
'utf-8'
)
expect(agentsMdContent).toContain(
'root: ./node_modules/next/dist/docs'
)
} finally {
process.chdir(originalCwd)
}
})

it('falls back to downloading when --version differs from the installed version', async () => {
setupBundledNext(testProjectDir, '16.2.0')

const originalCwd = process.cwd()
process.chdir(testProjectDir)

try {
await runAgentsMd({ version: '15.0.0', output: 'CLAUDE.md' })

expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
true
)

const claudeMdContent = fs.readFileSync(
path.join(testProjectDir, 'CLAUDE.md'),
'utf-8'
)
expect(claudeMdContent).toContain('root: ./.next-docs')

const output = consoleOutput.join('\n')
expect(output).toContain('Downloading')
} finally {
process.chdir(originalCwd)
}
}, 30000) // Increase timeout for git clone
})

describe('getNextjsVersion', () => {
const fixturesDir = path.join(__dirname, 'fixtures/agents-md')

Expand Down
36 changes: 36 additions & 0 deletions packages/next-codemod/lib/agents-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ export function getNextjsVersion(cwd: string): NextjsVersionResult {
}
}

interface BundledDocsInfo {
docsPath: string
version: string
}

/**
* Next.js ships its documentation inside the published package (at
* `dist/docs`) since 16.2.0. When the install resolved from `cwd` has
* bundled docs, the index can point at them directly instead of
* downloading a copy into `.next-docs`.
*/
export function getBundledDocsInfo(cwd: string): BundledDocsInfo | null {
try {
const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] })
const pkg = JSON.parse(fs.readFileSync(nextPkgPath, 'utf-8'))
const docsPath = path.join(path.dirname(nextPkgPath), 'dist', 'docs')
if (!pkg.version || collectDocFiles(docsPath).length === 0) {
return null
}
return { docsPath, version: pkg.version }
} catch {
return null
}
}

export function getBundledDocsLinkPath(cwd: string, docsPath: string): string {
// Prefer the conventional path when it resolves from the project
// (covers hoisted installs; pnpm exposes next via a node_modules symlink).
const conventional = path.join(cwd, 'node_modules', 'next', 'dist', 'docs')
if (fs.existsSync(conventional)) {
return './node_modules/next/dist/docs'
}
const relative = path.relative(cwd, docsPath).replace(/\\/g, '/')
return relative.startsWith('.') ? relative : `./${relative}`
}

function versionToGitHubTag(version: string): string {
return version.startsWith('v') ? version : `v${version}`
}
Expand Down
Loading
Loading