Next.js 15 Complete Guide | Turbopack, React 19, Partial Prerendering & New APIs

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:

MetricwebpackTurbopack
Server startupbaseline76% faster
Code updates (HMR)baseline96% 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 explicit cache: 'force-cache' where you relied on default caching
  • GET Route Handlers — add export const dynamic = 'force-static' if they should be cached
  • cookies(), headers() — add await
  • params and searchParams props — add await (they’re now Promises)
  • next.config.js → optionally rename to next.config.ts
  • Test all pages that rely on cached data

Related posts: