Next.js 15 Released - Turbopack Stabilization and React 19 Support

2025.12.02

Next.js 15 Overview

Next.js 15 is a major release featuring official React 19 support and Turbopack stabilization. Significant improvements have been made to both developer experience and performance.

flowchart TB
    subgraph Next15["Next.js 15 Highlights"]
        subgraph Turbo["Turbopack (Stable for Dev)"]
            T1["Dev server startup: 76% faster"]
            T2["Fast Refresh: 96% faster"]
            T3["Initial compile: 45% faster"]
        end

        subgraph React19["React 19 Support"]
            R1["Server Actions (Stable)"]
            R2["use() Hook"]
            R3["React Compiler (Experimental)"]
        end

        subgraph PPR["Partial Prerendering (Beta)"]
            P1["Static shell + dynamic content streaming"]
        end

        subgraph Cache["New Caching Defaults"]
            C1["fetch: no-store (default)"]
            C2["Route Handlers: no-store (default)"]
        end
    end

Turbopack Stabilization

In Next.js 15, Turbopack for the development server has become stable.

# Start dev server with Turbopack
next dev --turbo

# Build still uses Webpack (Turbopack in development)
next build

Performance Comparison

Large Project (5000 modules) Performance:

MetricWebpackTurbopackImprovement
Dev server startup8.2s2.0s76% faster
Fast Refresh (1 file change)800ms30ms96% faster
Initial route compile600ms330ms45% faster
Memory usage1.6GB800MB50% reduction

React 19 Support

Server Actions

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Validation
  if (!title || title.length < 3) {
    return { error: 'Title must be at least 3 characters' };
  }

  // Database operation
  await db.posts.create({ title, content });

  // Revalidate cache
  revalidatePath('/posts');

  // Redirect
  redirect('/posts');
}

// Action with optimistic updates
export async function likePost(postId: string) {
  await db.posts.update({
    where: { id: postId },
    data: { likes: { increment: 1 } },
  });

  revalidatePath(`/posts/${postId}`);
}
// app/posts/new/page.tsx
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

useActionState

'use client';

import { useActionState } from 'react';
import { createPost } from '../actions';

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);

  return (
    <form action={formAction}>
      <input name="title" placeholder="Title" disabled={isPending} />
      <textarea name="content" placeholder="Content" disabled={isPending} />

      {state?.error && (
        <p className="text-red-500">{state.error}</p>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

useOptimistic

'use client';

import { useOptimistic } from 'react';
import { likePost } from '../actions';

export function LikeButton({ postId, initialLikes }: Props) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, _) => state + 1
  );

  async function handleLike() {
    addOptimisticLike(null);
    await likePost(postId);
  }

  return (
    <form action={handleLike}>
      <button type="submit">
        ❤️ {optimisticLikes}
      </button>
    </form>
  );
}

Partial Prerendering (PPR)

A new rendering strategy combining static shell with dynamic content.

flowchart TB
    subgraph BuildTime["At Build Time"]
        subgraph StaticShell["Static Shell"]
            Header["Header (Static)"]
            subgraph Layout["Layout"]
                Content["Content (Static)"]
                Suspense["Suspense Fallback<br/>(Skeleton)"]
            end
        end
    end

    subgraph RequestTime["At Request Time"]
        Step1["1. Return static shell immediately (TTFB: few ms)"]
        Step2["2. Stream dynamic parts"]
        Step3["3. Suspense boundaries hydrate sequentially"]
        Step1 --> Step2 --> Step3
    end

    BuildTime --> RequestTime
// next.config.js
module.exports = {
  experimental: {
    ppr: true,
  },
};
// app/products/[id]/page.tsx
import { Suspense } from 'react';

// Statically generated part
export default function ProductPage({ params }: Props) {
  return (
    <div>
      {/* Static content */}
      <ProductDetails id={params.id} />

      {/* Dynamic content (streaming) */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations userId={getUserId()} />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <RecentReviews productId={params.id} />
      </Suspense>
    </div>
  );
}

// Component using dynamic data
async function PersonalizedRecommendations({ userId }: { userId: string }) {
  // This part executes at request time
  const recommendations = await getRecommendations(userId);
  return <RecommendationList items={recommendations} />;
}

New Caching Semantics

In Next.js 15, the default caching behavior has changed.

// Before Next.js 14: Cached by default
// Next.js 15: No cache by default

// fetch API default change
async function getData() {
  // Next.js 14: cache: 'force-cache' was default
  // Next.js 15: cache: 'no-store' is default
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

// Explicitly enable caching
async function getCachedData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache',
    next: { revalidate: 3600 }, // 1 hour
  });
  return res.json();
}

Route Handler Caching

// app/api/data/route.ts

// Next.js 14: GET is statically cached
// Next.js 15: No cache by default

// Enable caching
export const dynamic = 'force-static';
// or
export const revalidate = 3600;

export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}

Security Enhancements

Server Actions Security

// Server Actions endpoint protection
// next.config.js
module.exports = {
  experimental: {
    serverActions: {
      // Specify allowed origins
      allowedOrigins: ['my-app.com', 'staging.my-app.com'],
    },
  },
};
// app/actions.ts
'use server';

import { headers } from 'next/headers';

export async function sensitiveAction() {
  // Referer check
  const referer = headers().get('referer');
  if (!referer?.startsWith('https://my-app.com')) {
    throw new Error('Invalid origin');
  }

  // CSRF protection is automatic
  // ...
}

Static Export Improvements

// next.config.js
module.exports = {
  output: 'export',

  // Image optimization for static export
  images: {
    unoptimized: true,
    // Or use external loader
    loader: 'custom',
    loaderFile: './image-loader.js',
  },
};
// image-loader.js
export default function customLoader({ src, width, quality }) {
  return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`;
}

instrumentation.ts

Execute code at application bootstrap.

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    // Execute only in Node.js runtime
    await import('./monitoring/sentry');
  }

  if (process.env.NEXT_RUNTIME === 'edge') {
    // Execute only in Edge runtime
    await import('./monitoring/edge-logger');
  }
}

export function onRequestError(error: Error, request: Request) {
  // Send error logs
  console.error('Request error:', error);
}

Migration

# Upgrade
npm install next@15 react@19 react-dom@19

# Automatic codemod
npx @next/codemod@latest upgrade latest

Major Breaking Changes

ChangeNext.js 14Next.js 15
fetch defaultcache: ‘force-cache’cache: ‘no-store’
Route HandlerStatic cacheNo cache
React versionReact 18React 19
Node.js minimum18.17.018.18.0
← Back to list