[2026] Next.js Complete Guide — App Router Internals, RSC, Caching & Production Patterns
이 글의 핵심
A complete, internals-oriented guide to Next.js 15: how App Router differs from Pages Router, how React Server Components fit the request lifecycle, how static vs dynamic rendering is chosen, the four caching layers plus React cache(), and patterns that hold up in production.
What this guide covers
This article translates and expands the Korean Next.js 15 complete guide into English and adds framework internals: routing models, React Server Components (RSC), rendering modes, the caching stack, and production patterns that match how Next.js 15 actually behaves.
Practical context: Examples align with real migrations—Turbopack for faster
next dev, Server Actions to shrink client bundles, and explicit cache contracts after the Next.js 15 default changes.
Introduction: what changed in Next.js 15?
Released in October 2024, Next.js 15 emphasizes performance, developer experience, and predictable caching.
Highlights:
- React 19 support
- Turbopack stabilized for development (large cold-start wins vs webpack)
- Experimental Partial Prerendering (PPR)
- Stable Server Actions
- Caching defaults that favor freshness (
fetchno longer cached by default) - Async dynamic APIs:
cookies(),headers(), and routeparams/searchParamsas Promises
flowchart TB
subgraph v14[Next.js 14]
A1[Pages Router still common]
A2[fetch cached by default]
A3[webpack dev server]
end
subgraph v15[Next.js 15]
B1[App Router first-class]
B2[fetch uncached by default]
B3[Turbopack dev default]
B4[React 19]
B5[PPR experimental]
end
v14 --> v15
Pain points this guide addresses
1. Dev server is too slow — Turbopack (next dev, often default) reduces cold compile time dramatically on large apps.
2. Static pages hit the server every time — After the caching default change, you must opt in to cached fetch or segment revalidate so behavior matches your product expectations.
3. Too much client code for forms — Server Actions move mutations to the server without hand-written JSON APIs for many use cases.
App Router vs Pages Router: routing and data at different layers
Pages Router (pages/) is the classic model: each file is one page, and data enters through getServerSideProps, getStaticProps, or getInitialProps at the page boundary. Global layout is centralized in _app / _document; nested route-specific layouts and shared UI state are less natural in the framework.
App Router (app/) assumes React Server Components by default. layout.tsx nests per segment; page.tsx is a leaf in that tree. Data is fetched inside async Server Components (or via Route Handlers / Server Actions) instead of exporting a parallel data function from the page file. The mental model is component-tree boundaries, not “one page, one data hook.”
| Topic | Pages Router | App Router |
|---|---|---|
| Files | pages/foo.tsx → /foo | app/foo/page.tsx → /foo |
| Layout | Single _app wrapper | Nested layout.tsx per segment |
| Data | GSSP / GSP / legacy patterns | async Server Components + fetch / DB |
| Client JS | Hydration per page | RSC payload + 'use client' islands |
| HTTP handlers | pages/api/* | app/**/route.ts Route Handlers |
Recommendation: choose App Router for greenfield apps. You can run both routers in one project during migration, but keep URL namespaces, middleware, and i18n rules explicit to avoid duplicate or conflicting routes.
React Server Components architecture
In the App Router, components are Server Components unless marked with 'use client'. Server Components:
- Run only on the server (or in the server layer of your deployment).
- Cannot use
useState,useEffect, or browser-only APIs. - Produce an RSC payload that streams to the client; Client Components hydrate at the
'use client'boundary.
Rough lifecycle
- The server renders the RSC tree and emits a serializable payload (streamable).
- The client hydrates Client Component boundaries; server output is merged from the payload.
'use client'modules (and their imports) weigh on the JS bundle—keep the boundary shallow to minimize bytes.
Typical split: database access, secrets, and heavy transforms stay in Server Components; interactive widgets (forms, charts, dialogs) become Client Components. Server Actions expose typed server mutations without a separate REST layer for many workflows.
Static vs dynamic rendering: how Next.js decides
Whether a route’s HTML/RSC output is fixed at build time or computed per request depends on the combination of:
- Segment config:
export const dynamic = 'force-static' | 'force-dynamic' | 'auto'. - Dynamic APIs: reading
cookies(),headers(), or uncachedsearchParams/paramspatterns tends to push the segment toward dynamic rendering. In Next.js 15,cookies,headers, andparamsare async—how and where youawaitthem participates in the static/dynamic analysis. fetchsemantics:cache: 'no-store'ornext: { revalidate: 0 }implies fresh data and often a dynamic route.cache: 'force-cache'ornext: { revalidate: N }tilts toward static or ISR.- Segment
revalidate:export const revalidate = 3600declares time-based ISR intent for that segment.
Rule of thumb: marketing and docs → static or ISR. Authenticated dashboards and personalization → dynamic. A static shell with streamed dynamic regions → Suspense boundaries ± experimental PPR.
Caching layers: Request memoization, Data Cache, Full Route Cache, Router Cache
Next.js documents four distinct caches. Treat them as separate mechanisms—not one big “cache object.”
flowchart TB
subgraph per["1. Request Memoization (React)"]
M["Dedupe identical fetch URLs in one render pass"]
end
subgraph data["2. Data Cache"]
D["Persistent fetch results when opted in"]
end
subgraph full["3. Full Route Cache"]
F["HTML + RSC payload for static routes"]
end
subgraph router["4. Router Cache (client)"]
R["Soft navigation reuse of shared segments"]
end
per --> data
data --> full
router --> R
| Layer | Lifetime | What it reduces |
|---|---|---|
| Request Memoization | Single request | Duplicate fetch with the same URL in one tree |
| Data Cache | Across requests (when enabled) | Repeated remote work for the same cached fetch |
| Full Route Cache | Static route output | Regenerating full HTML/RSC for static pages |
| Router Cache | Client session (soft nav) | Refetching shared layouts during SPA-style navigations |
React cache(): for non-fetch work (Prisma, ORMs, raw DB), wrap read functions with import { cache } from 'react' so the same render pass does not repeat identical queries. This is not the same as the Data Cache or fetch memoization.
Router Cache vs freshness: Next.js 15 adjusted client caching defaults toward fresh data. If a view must always reflect the latest server state after navigation, combine explicit fetch options, router.refresh(), and/or revalidatePath / revalidateTag in Server Actions.
Production-ready Next.js patterns
- Segment boundaries: colocate
loading.tsx,error.tsx, andnot-foundto isolate slow or failing subtrees. - Secrets: keep secrets in server-only modules, Server Actions, and Route Handlers; expose only
NEXT_PUBLIC_*to the browser. - After-response work: use
unstable_after(see below) for logging and analytics without blocking the response. - Runtime choice: Edge vs Node runtimes differ in APIs and limits—pick based on latency, geo, and library support.
- Cache contract as documentation: under “fetch is uncached by default,” write down what may be stale and for how long, and align invalidation with
revalidateTag.
1. Install and migrate
New project
npx create-next-app@latest my-app
cd my-app
npm run dev
Recommended options: TypeScript, ESLint, Tailwind (optional), src/ directory, App Router, Turbopack for dev.
Upgrade an existing app
npm install next@latest react@latest react-dom@latest
{
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Breaking changes (short list)
Async request APIs
// Before (Next.js 14 style — synchronous accessors)
import { cookies, headers } from 'next/headers';
export default function Page() {
const cookieStore = cookies();
const headersList = headers();
}
// After (Next.js 15)
import { cookies, headers } from 'next/headers';
export default async function Page() {
const cookieStore = await cookies();
const headersList = await headers();
}
fetch default
// Next.js 14: effectively cached unless opted out
// Next.js 15: uncached by default — opt in explicitly
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 },
});
GET Route Handlers — not cached by default; use export const dynamic = 'force-static' or revalidate when a cached response is intended.
params / searchParams as Promises
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <article>{id}</article>;
}
2. Turbopack for faster development
Turbopack is a Rust-based dev bundler. In Next.js 15 it is the default for next dev in many setups; it dramatically improves cold start and HMR vs webpack.
next dev
# explicit
next dev --turbo
# opt out of Turbopack
next dev --no-turbo
Observed effects on large codebases: multi-second → sub-second HMR; cold dev compile time often drops by an order of magnitude compared to webpack-only workflows.
3. Server Actions
Server Actions are async functions that run on the server and can be invoked from forms and (with care) from client code. They replace many ad hoc POST handlers for mutations.
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({ data: { title, content } });
revalidatePath('/posts');
redirect('/posts');
}
// app/new-post/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Publish</button>
</form>
);
}
Progressive enhancement with useFormState / useFormStatus (client) pairs well with zod validation in the action.
Security: always re-check auth and ownership on the server—never trust the client payload alone.
4. Partial Prerendering (PPR)
PPR (experimental) combines a static shell (built ahead) with dynamic regions resolved at request time and streamed behind Suspense.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
ppr: 'incremental',
},
};
export default nextConfig;
Use Suspense to mark dynamic boundaries; avoid assuming every region is static unless the compiler can prove it.
5. React 19 features used with Next.js
Notable APIs: Actions, useOptimistic, use for promises, and improved form status hooks. use enables client components to suspend on promises passed from the server.
'use client';
import { useOptimistic } from 'react';
import { likePost } from './actions';
export function LikeButton({
postId,
initialLikes,
}: {
postId: string;
initialLikes: number;
}) {
const [likes, addOptimistic] = useOptimistic(initialLikes, (s, n: number) => s + n);
async function onClick() {
addOptimistic(1);
await likePost(postId);
}
return <button onClick={onClick}>Likes {likes}</button>;
}
6. Caching strategy (putting it together)
fetch options
const a = await fetch(url); // Next.js 15: uncached by default
const b = await fetch(url, { next: { revalidate: 3600 } });
const c = await fetch(url, { cache: 'force-cache' });
const d = await fetch(url, { cache: 'no-store' });
On-demand revalidation
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updatePost(id: string, data: unknown) {
await db.post.update({ where: { id }, data });
revalidatePath('/posts');
revalidateTag('posts');
}
await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
Segment static configuration
export const dynamic = 'force-static';
export const dynamic = 'force-dynamic';
export const revalidate = 3600;
7. Performance patterns
next/imagewith explicitsizesand blur placeholders for LCP.next/fontto self-host fonts and reduce layout shift.Suspensefor streaming SSR.next/dynamicfor heavy client-only modules.
8. Example: full-stack blog sketch
Typical layout:
app/
├── (auth)/login/page.tsx
├── (main)/posts/page.tsx
├── (main)/posts/[id]/page.tsx
├── api/posts/route.ts
├── actions.ts
└── layout.tsx
Use Prisma (or similar) on the server, Server Actions for creates/deletes, and revalidatePath after writes. For the post detail page, use async params and Suspense for comments.
9. Common mistakes
cookies / headers: must await in Next.js 15.
Cached fetch: restore explicit caching after upgrade.
redirect in try/catch: redirect throws—do not catch it in the same block you use for business errors.
Turbopack compatibility: some packages need aliases or webpack fallback—consult the library and Next.js docs.
10. Deploy and observability
npm i -g vercel
vercel
vercel --prod
unstable_after — schedule work after the response is sent (logging, analytics) without delaying TTFB:
import { unstable_after as after } from 'next/server';
export async function GET() {
const data = await loadData();
after(async () => {
await auditLog('data_read');
});
return Response.json(data);
}
Add Speed Insights / Analytics if you use Vercel; otherwise wire your own RUM.
Summary
- App Router + RSC change where data and JS live; Pages Router is legacy-first for many codebases but not the default for new apps.
- Static vs dynamic follows segment config, dynamic APIs, and
fetchsemantics together—not a single switch. - Four caches solve different problems; React
cache()solves server-side deduplication for non-fetch work. - Next.js 15 defaults favor freshness; document cache contracts explicitly and revalidate on writes.
Migration checklist
-
await cookies(),await headers(),await params/searchParamswhere required - Audit every
fetchfor explicit cache behavior - Audit GET Route Handlers for caching expectations
- Enable Turbopack dev and fix incompatible packages
- Validate Server Actions authz paths
- Measure LCP, TTFB, and bundle size after changes
FAQ
Is Turbopack safe for production builds?
Turbopack is stable for next dev. Production builds still typically use webpack unless you opt into experimental Turbopack for next build—verify against your dependencies.
Server Actions vs Route Handlers?
Use Server Actions for form posts and same-origin mutations tied to UI. Prefer Route Handlers for public HTTP APIs, webhooks, non-HTML clients, or complex content negotiation.
Does PPR always make pages faster?
PPR helps when a large share of the UI is static and smaller regions are dynamic. If everything is dynamic, conventional SSR with Suspense may be simpler.
Keywords
Next.js, App Router, React Server Components, Server Actions, Turbopack, Partial Prerendering, SSR, ISR, caching, Full Route Cache, Data Cache, Router Cache