Next.js 15 Complete Guide | Turbopack, React 19, Partial Prerendering & New APIs
이 글의 핵심
Next.js 15 ships stable Turbopack (76% faster dev server), React 19 support, Partial Prerendering for hybrid static+dynamic pages, and important breaking changes to caching defaults. This guide covers every change with migration examples.
What’s New in Next.js 15
Next.js 14 → 15 key changes:
✅ Turbopack stable for dev (next dev --turbo)
✅ React 19 support
✅ Partial Prerendering (experimental)
⚠️ fetch() no longer cached by default (breaking!)
⚠️ GET Route Handlers no longer cached by default
⚠️ Client Router Cache no longer stale by default
✅ unstable_after() — run code after response
✅ Improved error UI with source maps in browser
✅ next.config.ts (TypeScript support)
Upgrade
npx @next/upgrade latest
# Or manually:
npm install next@latest react@latest react-dom@latest
npm install -D @types/react@latest @types/react-dom@latest
Turbopack Dev Server
# Next.js 15: Turbopack is now the default for next dev
next dev
# Explicitly use Turbopack (same as default)
next dev --turbo
# Use webpack (opt-out)
next dev --no-turbo
Performance vs webpack:
| Metric | webpack | Turbopack |
|---|---|---|
| Server startup | baseline | 76% faster |
| Code updates (HMR) | baseline | 96% faster |
| Large app cold start | ~15-60s | ~2-5s |
Turbopack uses Rust-based incremental bundling — only recompiles what changed.
Breaking Change: Fetch Caching Defaults
The biggest breaking change in Next.js 15:
// Next.js 14 default behavior
fetch('https://api.example.com/data')
// Equivalent to: cache: 'force-cache' (cached indefinitely)
// Next.js 15 default behavior
fetch('https://api.example.com/data')
// Equivalent to: cache: 'no-store' (NOT cached, fetches fresh every request)
Migration — be explicit:
// Static (cached, like Next.js 14 behavior)
fetch('https://api.example.com/data', {
cache: 'force-cache',
});
// ISR (cached with revalidation)
fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Revalidate every hour
});
// Dynamic (uncached — same as new default)
fetch('https://api.example.com/data', {
cache: 'no-store',
});
Similarly for Route Handlers:
// Next.js 14: GET handlers cached by default
// Next.js 15: GET handlers NOT cached by default
// app/api/products/route.ts
// To restore caching:
export const dynamic = 'force-static';
// Or:
export const revalidate = 3600;
Partial Prerendering (PPR)
PPR enables a single route to be both static and dynamic simultaneously:
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
experimental: {
ppr: 'incremental', // Enable per-route, not globally
},
};
export default config;
// app/product/[id]/page.tsx
export const experimental_ppr = true; // Enable PPR for this route
import { Suspense } from 'react';
// Static shell — rendered at build time, instant
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<main>
<StaticHeader /> {/* Rendered at build time */}
<StaticNav /> {/* Rendered at build time */}
{/* Dynamic content — streamed at request time */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={params.id} /> {/* Fetches from DB at request time */}
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<PersonalizedRecommendations /> {/* Uses cookies/user data */}
</Suspense>
</main>
);
}
PPR rendering flow:
1. Build time: Static shell generated (header, nav, skeletons)
2. Request: Shell served immediately from CDN (fast TTFB)
3. Streaming: Dynamic content streamed as data loads
React 19 Features in Next.js 15
use() Hook for Promises
'use client';
import { use, Suspense } from 'react';
// Pass a Promise to a Client Component
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}
// Server Component creates the promise
async function Page() {
const userPromise = getUser(1); // Don't await — pass the promise
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Improved useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button disabled={pending} type="submit">
{pending ? 'Saving...' : 'Save'}
</button>
);
}
Server Actions Improvements
// Optimistic updates with useOptimistic (React 19)
'use client';
import { useOptimistic } from 'react';
import { toggleLike } from './actions';
function LikeButton({ post }: { post: Post }) {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(post.liked);
async function handleLike() {
setOptimisticLiked(!optimisticLiked); // Update UI immediately
await toggleLike(post.id); // Then sync with server
}
return (
<button onClick={handleLike}>
{optimisticLiked ? '❤️' : '🤍'}
</button>
);
}
unstable_after — Run Code After Response
Run cleanup/logging after the response is sent to the user — without blocking the response:
// app/api/products/route.ts
import { unstable_after as after } from 'next/server';
export async function GET(request: Request) {
const products = await getProducts();
// Schedule this to run after response is sent
after(async () => {
await logRequest({
path: '/api/products',
duration: Date.now() - start,
cached: false,
});
await updateAnalytics('product_list_viewed');
});
return Response.json(products);
}
next.config.ts — TypeScript Config
// next.config.ts (new in Next.js 15 — TypeScript support!)
import type { NextConfig } from 'next';
const config: NextConfig = {
experimental: {
ppr: 'incremental',
reactCompiler: true, // React Compiler (experimental)
},
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com' },
],
},
// Moved from next.config.js — fully typed
headers: async () => [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
],
},
],
};
export default config;
Async Request APIs (Breaking Change)
Headers, cookies, and params are now async in Next.js 15:
// Next.js 14
import { cookies, headers } from 'next/headers';
const cookieStore = cookies();
const headersList = headers();
// Next.js 15 — must await
import { cookies, headers } from 'next/headers';
const cookieStore = await cookies();
const headersList = await headers();
// Route params are also async
// Next.js 14
export default function Page({ params }: { params: { id: string } }) {
console.log(params.id);
}
// Next.js 15
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
console.log(id);
}
Improved Error UI
Next.js 15 includes a redesigned error overlay in development:
- Source maps now point to original TypeScript source (not compiled JS)
- Error frames are collapsible
- Better hydration error messages with diffs showing what changed
- Next.js-specific errors link directly to relevant docs
Migration Checklist: Next.js 14 → 15
# 1. Upgrade
npx @next/upgrade latest
# 2. Check codemod for automated fixes
npx @next/codemod@canary upgrade latest
Manual checks:
-
fetch()calls — add explicitcache: 'force-cache'where you relied on default caching - GET Route Handlers — add
export const dynamic = 'force-static'if they should be cached -
cookies(),headers()— addawait -
paramsandsearchParamsprops — addawait(they’re now Promises) -
next.config.js→ optionally rename tonext.config.ts - Test all pages that rely on cached data
Related posts: