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:
| Metric | Webpack | Turbopack | Improvement |
|---|---|---|---|
| Dev server startup | 8.2s | 2.0s | 76% faster |
| Fast Refresh (1 file change) | 800ms | 30ms | 96% faster |
| Initial route compile | 600ms | 330ms | 45% faster |
| Memory usage | 1.6GB | 800MB | 50% 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
| Change | Next.js 14 | Next.js 15 |
|---|---|---|
| fetch default | cache: ‘force-cache’ | cache: ‘no-store’ |
| Route Handler | Static cache | No cache |
| React version | React 18 | React 19 |
| Node.js minimum | 18.17.0 | 18.18.0 |