Next.js Optimization Recipes
PLTo get correct Next.js 16 code from an AI agent, make three moves: point the agent at the docs bundled in your installed next package so it stops writing Next.js 14 and 15 code, connect it to the running dev server through next-devtools-mcp, and let next build, tsc and Playwright tests prove each change. The recipes target Next.js 16 and the App Router.
This page is for developers who let Claude Code, Codex or Cursor write a Next.js App Router application, and for the tech lead who has to trust the result. The agent’s dashboard renders, but it creates middleware.ts with export const runtime = 'edge', calls revalidateTag('posts') with one argument, and puts 'use client' at the top of a page. Everything compiles, the build warns, and nobody reads the warnings.
What these Next.js recipes give you
Section titled “What these Next.js recipes give you”- A context setup that makes the agent read version-matched Next.js docs instead of its training data
- The Next.js DevTools MCP server and the
next-dev-loopskill wired into all three tools, so the agent sees runtime errors itself - Eight prompts for the App Router work agents most often get wrong: streaming, Server Actions, auth, modals, caching, Proxy, Core Web Vitals, and tenant isolation
- A gate table that signs a change off without anyone reading every line, and a failure-mode table with recovery steps
Give the agent version-matched Next.js docs first
Section titled “Give the agent version-matched Next.js docs first”Agents learned Next.js from years of Pages Router, Next.js 13 to 15, and Vercel blog posts. Since Next.js 16.2, the next package ships its own docs in node_modules/next/dist/docs/, matched to the version you installed. Your job is to make the agent read them before it writes code.
-
Check which case applies. On Next.js 16.3 or later,
next devdetects a coding agent and writes a managed block intoAGENTS.md, plus aCLAUDE.mdthat contains@AGENTS.md. Commit both files. On 16.2, write theAGENTS.mdinstruction yourself. On 16.1 or earlier, runnpx @next/codemod@canary agents-mdin the project root to download version-matched docs into.next-docs/. -
Add your project rules to
AGENTS.mdoutside the<!-- BEGIN:nextjs-agent-rules -->markers.next devrewrites everything inside them.## Next.js rules for this repo (Next.js 16.3, App Router only)- Read node_modules/next/dist/docs/ before using any Next.js API. Heed deprecation notices.- Request interception lives in proxy.ts (export `proxy`). Never create middleware.ts.- No `export const runtime = 'edge'`. The Edge Runtime is deprecated.- cacheComponents is on: use 'use cache' + cacheLife + cacheTag, never `revalidate`,`dynamic` or `dynamicParams` segment exports.- revalidateTag(tag, 'max') in Route Handlers; updateTag(tag) in Server Actions.- 'use client' only on leaf interactive components, never on page.tsx or layout.tsx.- Every Server Action checks the session and the user's permission itself.- Done means: next typegen && tsc --noEmit, eslint, next build and Playwright all pass. -
Put the stale patterns in front of the reviewer too. These are the APIs the agent reaches for first, checked against the bundled docs of
next@16.3.6:Agent writes Status in Next.js 16 Write instead middleware.tswithexport function middlewareDeprecated, renamed in 16.0.0 proxy.tswithexport function proxy(codemod:npx @next/codemod@canary middleware-to-proxy .)export const runtime = 'edge'Edge Runtime deprecated; not supported in proxy.tsor with Cache ComponentsRemove the export; Node.js is the default revalidateTag('posts')One-argument form deprecated, TypeScript error revalidateTag('posts', 'max'), orupdateTag('posts')inside a Server Actionexperimental.pprorexperimental_pprRemoved in 16 cacheComponents: trueinnext.config.tsexport const revalidate = 3600on a pageRemoved when Cache Components is on 'use cache'withcacheLife('hours')request.geo,request.ipRemoved in 15 Your host’s headers, or geolocation()from@vercel/functionson Verceldynamic(() => import(...), { ssr: false })in a Server ComponentBuild error Move the call into a Client Component next lintRemoved in 16 eslintdirectly (codemod:next-lint-to-eslint-cli)A parallel route slot without default.tsxBuild fails in 16 A default.tsxthat returnsnullor callsnotFound()<Image priority>Deprecated in 16.0.0 fetchPriority="high"orloading="eager"(orpreload) on the one LCP image
Connect the agent to the running Next.js dev server
Section titled “Connect the agent to the running Next.js dev server”A type check proves the code compiles. It does not show the hydration error in the browser console or the route that blocks prerendering. Next.js 16 dev servers expose an MCP endpoint at /_next/mcp, and the next-devtools-mcp package (0.4.0) connects agents to it. Its tools include get_errors, get_routes and get_logs, plus two compile tools that work without a full next build: compile_route, which compiles one route on demand, and get_compilation_issues, which returns the project’s current compile errors. Both compile tools are Turbopack only. See dev-server MCP servers for Next.js and other frameworks for the details.
# Terminal, in the project root. -s project writes .mcp.json so the team shares it.claude mcp add -s project next-devtools -- npx -y next-devtools-mcp@latestnpx skills add vercel/next.js --skill next-dev-loopStart npm run dev in a second terminal, then run /mcp in the session to confirm next-devtools is connected.
# Terminal. Writes the server to ~/.codex/config.toml.codex mcp add next-devtools -- npx -y next-devtools-mcp@latestnpx skills add vercel/next.js --skill next-dev-loopRun codex mcp list to confirm the entry, and keep npm run dev running while the session works.
Add the same server to .cursor/mcp.json in the project root, then install the skill from a terminal with npx skills add vercel/next.js --skill next-dev-loop.
{ "mcpServers": { "next-devtools": { "command": "npx", "args": ["-y", "next-devtools-mcp@latest"] } }}next-dev-loop needs Next.js 16.3 or later on Turbopack and the agent-browser CLI 0.31.1 or later: npm install -g agent-browser && agent-browser install. When either is missing, the skill tells you how to upgrade and stops instead of running.
The next-dev-loop skill turns that connection into an inspect, edit and verify cycle, using the MCP server and the agent-browser CLI for the browser’s view. For page-level browser checks beyond that, add Playwright MCP or the Playwright CLI. The other Next.js skills in vercel/next.js (next-cache-components-adoption, next-cache-components-optimizer, next-partial-prefetching-adoption) are listed with the rest of the frontend skills.
Stream a dashboard with independent Suspense boundaries
Section titled “Stream a dashboard with independent Suspense boundaries”Your dashboard makes four independent data calls, and the page waits for the slowest one. The fix is one <Suspense> boundary per section. The failure you are preventing is the agent wrapping everything in Promise.all or moving the page to the client.
The instant() helper from @next/playwright scopes assertions to the UI available the moment the user navigates. The test fails if a later change makes the route block again, which is the regression a code review almost never catches.
Build forms with Server Actions that check authorization
Section titled “Build forms with Server Actions that check authorization”Server Actions look like function calls, but the Next.js docs are explicit that they are “reachable via direct POST requests”. An agent that checks the session in proxy.ts and trusts it inside the action leaves the endpoint open to anyone who replays the request.
For an action that changes the signed-in user’s own data, add one line to the prompt: “call updateTag for the tag the page reads, so the user sees the change on the next render”. updateTag works only inside Server Actions; a webhook Route Handler uses revalidateTag(tag, 'max').
Protect routes with Proxy and a data access layer
Section titled “Protect routes with Proxy and a data access layer”Auth is where agents blur the server and client boundary most. In Next.js 16, proxy.ts runs on the Node.js runtime and the docs recommend it only for optimistic checks, reading the session cookie without a database call. The real check belongs next to the data. Pick one library before you prompt: Auth.js v5 is still on the beta tag, and Better Auth (better-auth 1.7.6) is the other common choice for App Router projects.
Start in plan mode (/plan) so you review the file list before any code exists. After the change, run /code-review and ask it to list every Server Action and Route Handler that does not call verifySession(). For CI, the same audit runs headless with claude -p "…" --tools "Read,Grep,Glob" so the reviewer cannot edit what it audits.
Run the change in its own worktree so a half-written proxy.ts never breaks your running dev server: codex exec --worktree --sandbox workspace-write "…". Then ask a second, read-only session (codex exec --sandbox read-only) for the same list of unprotected actions and handlers.
Use Plan Mode for the file list, then let the agent implement it in a worktree. Review the diff of proxy.ts and lib/dal.ts in the editor. Those two files hold every decision; the rest is wiring. Bugbot on the pull request is a second reviewer, not the gate.
Open product details in a modal with intercepting routes
Section titled “Open product details in a modal with intercepting routes”Product details should open in a modal from the grid and as a full page on a direct visit or refresh. Agents usually get the folder names right and miss two things: the default.tsx that Next.js 16 now requires for every parallel slot, and the behaviour on refresh.
Cache a blog with Cache Components and on-demand revalidation
Section titled “Cache a blog with Cache Components and on-demand revalidation”The old recipe for a 500-post blog was Incremental Static Regeneration with export const revalidate. With cacheComponents: true, those segment exports are gone. Caching is declared on the data function with 'use cache', and a CMS webhook expires the right tag.
Run A/B tests and feature flags in proxy.ts
Section titled “Run A/B tests and feature flags in proxy.ts”Proxy is the right place for a cookie-based variant split and for passing flags to Server Components as request headers. It is the wrong place for anything slow, because it runs on every matched request, prefetches included.
Fix Core Web Vitals with a budget CI enforces
Section titled “Fix Core Web Vitals with a budget CI enforces”Lighthouse reports a performance score of 65, LCP of 4.2 s and CLS of 0.25. The edits are routine: next/image with fetchPriority="high" on the one LCP image, next/font, skeletons with fixed dimensions, and useTransition on the filter. The part that lasts is the budget in CI, so the next agent-written change cannot undo them.
Add the Chrome DevTools MCP server (claude mcp add chrome-devtools --scope user npx chrome-devtools-mcp@latest, version 1.10.1) so the agent records a performance trace against http://localhost:3000 and reads the LCP element itself, instead of guessing which image it is.
Run the fix as codex exec --worktree with the prompt below, then open the pull request with the before and after Lighthouse numbers in its description. The worktree keeps the performance branch apart while the budgets settle.
Let the agent sweep the component edits across files and check each diff for fetchPriority="high": it belongs on one image per page. High priority on every image slows LCP down instead of speeding it up. If the agent writes priority, replace it: Next.js 16 deprecated that prop.
Lighthouse is a lab test with no user input, so it cannot measure Interaction to Next Paint. Total blocking time is the lab proxy; field INP comes from useReportWebVitals in real sessions. Google’s web.dev Core Web Vitals thresholds (checked 2026-09-26) put “good” at LCP up to 2.5 s, CLS up to 0.1 and INP up to 200 ms. For a load-testing setup beyond the page, see performance testing with agents.
Isolate tenants in a multi-tenant SaaS
Section titled “Isolate tenants in a multi-tenant SaaS”A SaaS that routes tenants by subdomain fails in one way that matters: one tenant sees another tenant’s rows. A tenant-scoped query wrapper plus a test that tries to cross the boundary is the only proof that holds after the agent’s next refactor.
For end-to-end type safety between server and client, tRPC 11 (@trpc/server 11.19.0) remains an option. Server Actions and Route Handlers with PageProps types cover most App Router apps without it. Whichever you choose, the proof is the same: change a server return type and confirm tsc --noEmit fails in the consuming component.
How to verify agent-written Next.js code without reading every line
Section titled “How to verify agent-written Next.js code without reading every line”Read the diffs of proxy.ts, lib/dal.ts, next.config.ts and any file that exports a Server Action. Let the gates prove the rest.
| Gate | Command or tool | What goes red |
|---|---|---|
| Types | npx next typegen && npx tsc --noEmit | Sync params, wrong PageProps, a one-argument revalidateTag, a server type change the client did not follow |
| Lint | npx eslint . plus no-restricted-imports for server-only modules and the raw database client | Server code imported into a Client Component; tenant queries that skip the scoped client |
| Build | npx next build (add --debug-prerender when the build error is not enough) | Routes that block prerendering, ssr: false in a Server Component, a slot without default.tsx |
| Runtime | get_errors and compile_route through next-devtools-mcp | Hydration errors and runtime errors that type checks miss |
| Behaviour | Playwright, with instant() from @next/playwright for navigations | Auth bypass, modal navigation, streaming regressions, tenant crossing |
| Performance | npx @lhci/cli autorun with the budgets above | LCP and CLS regressions, heavy JavaScript |
Make the agent run the same gates before it says it is done. In Claude Code, add a Stop hook to .claude/settings.json that runs npx tsc --noEmit && npx next build and exits with code 2 when either fails. Exit code 2 sends the hook’s stderr back to the agent instead of ending the turn, so the command redirects the build output to stderr:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "cd \"$CLAUDE_PROJECT_DIR\" && { npx tsc --noEmit && npx next build; } 1>&2 || exit 2", "timeout": 600 } ] } ] }}Stop takes no matcher. To stop a failing build from looping, check stop_hook_active in a script instead; see hooks automation. In Codex and Cursor, put the “Done means” line from the rules block in AGENTS.md or your Rules and require the command output in the summary. The developer who owns the pull request signs off on the gate results and the four files above; CI enforces the rest. For how that review works at team scale, see reviewing agent pull requests.
When agent-written Next.js code breaks
Section titled “When agent-written Next.js code breaks”| Symptom | Cause | Recovery |
|---|---|---|
Whole page is client-rendered; generateMetadata has no effect | 'use client' on page.tsx or layout.tsx | Move the directive to the smallest interactive leaf; add an ESLint rule or grep in CI that fails on 'use client' in page.tsx and layout.tsx |
Build warns that middleware is deprecated, or fails with a runtime config error in proxy.ts | Agent wrote Next.js 15 code | npx @next/codemod@canary middleware-to-proxy ., delete the runtime export, and confirm AGENTS.md still has the managed block |
| A mutation shows stale data | revalidateTag with a profile where the user needs to see their own write | Use updateTag inside the Server Action; keep revalidateTag(tag, 'max') for webhooks |
| Admin action works for a normal user through a crafted POST | Authorization only in proxy.ts | Add requireRole() inside the action; add the direct-POST Playwright test from the auth recipe |
| Server Action or page fails with an “Only plain objects” serialization error | A class instance (an ORM entity, a Decimal) or a function crossed the server–client boundary | Map results to plain objects in the data layer before they reach a Client Component or leave an action; add a unit test that asserts the return value is a plain object |
| CSS or images stop loading after a Proxy change | Matcher runs on _next/static and _next/image | Restore the negative matcher; add a Playwright check that a page’s stylesheet returns 200 |
| Agent keeps producing Next.js 14 code in a new session | AGENTS.md block deleted, or the docs are not bundled (16.1 or earlier) | Run next dev from the agent’s session (or npx @next/codemod@canary agents-md on 16.1 and earlier) to restore the block, then commit it |
Where to go next with Next.js and agents
Section titled “Where to go next with Next.js and agents”- Start from the frontend recipes overview if you also work in Angular, Vue or Nuxt.
- Take component-level patterns further with React development recipes.
- Write the acceptance criteria your Playwright tests encode with acceptance criteria agents can verify.
- Build the end-to-end suite the gates rely on with end-to-end testing with agents.
- Give agents current docs for the rest of your stack with Context7.
- Ship the app in a container with Docker patterns, or design the API behind it with API development patterns.
Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in