Next.js 15 has been released with many innovative features including React 19 support and stable Turbopack. This article explains the major changes in Next.js 15 that developers need to know and how to migrate.
Key New Features Summary
| Feature | Status | Impact |
|---|---|---|
| React 19 Support | Stable | High |
| Turbopack Dev | Stable | High |
| Partial Prerendering (PPR) | Experimental | Medium |
| next/after API | Stable | Medium |
| Cache Behavior Changes | Breaking | High |
| ESLint 9 Support | Stable | Low |
React 19 Support
Integration with React 19 RC
Next.js 15 is tightly integrated with React 19.
# Create new project
npx create-next-app@latest my-app
# Upgrade existing project
npm install next@latest react@rc react-dom@rc
Enhanced Actions
React 19’s useActionState and useFormStatus are now natively supported.
// app/actions.ts
'use server';
export async function submitForm(prevState: any, formData: FormData) {
const email = formData.get('email');
// Validation
if (!email || typeof email !== 'string') {
return { error: 'Please enter a valid email address' };
}
// Save to database
await saveToDatabase(email);
return { success: true, message: 'Registration complete' };
}
// app/form.tsx
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { submitForm } from './actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Register'}
</button>
);
}
export function NewsletterForm() {
const [state, formAction] = useActionState(submitForm, null);
return (
<form action={formAction}>
<input type="email" name="email" placeholder="Email address" />
<SubmitButton />
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">{state.message}</p>}
</form>
);
}
React Compiler (Experimental)
React Compiler eliminates the need for manual useMemo and useCallback.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default nextConfig;
# Install Babel plugin
npm install babel-plugin-react-compiler
Turbopack Dev Stable
Dramatic Performance Improvement
Turbopack is now stable for development mode (next dev).
# Start dev server with Turbopack enabled
next dev --turbo
Benchmark Results
Next.js 15 + Turbopack vs Webpack:
| Metric | Webpack | Turbopack |
|---|---|---|
| Local server startup | 4.2s | 1.1s |
| Initial compile | 8.5s | 2.3s |
| Fast Refresh | 320ms | 45ms |
| Route update | 180ms | 25ms |
Measured on large application (1000+ components)
Support Status
// Current Turbopack support status
const turbopackSupport = {
development: 'stable', // Stable
production: 'coming soon', // Coming soon
features: {
appRouter: 'full',
pagesRouter: 'full',
cssModules: 'full',
tailwindCSS: 'full',
sassScss: 'full',
mdx: 'full',
nextImage: 'full',
nextFont: 'full',
},
};
Partial Prerendering (PPR)
Optimal Combination of Static and Dynamic
PPR is an experimental feature that efficiently combines static and dynamic parts on the same page.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
ppr: 'incremental', // Enable per route
},
};
export default nextConfig;
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StaticHeader } from './static-header';
import { DynamicContent } from './dynamic-content';
// Enable PPR
export const experimental_ppr = true;
export default function Dashboard() {
return (
<div>
{/* Statically prerendered */}
<StaticHeader />
{/* Dynamically streamed */}
<Suspense fallback={<LoadingSkeleton />}>
<DynamicContent />
</Suspense>
</div>
);
}
PPR Flow
flowchart TB
subgraph BuildTime["At Build Time"]
StaticShell["Static Shell (Header, Layout, Fallback)<br/>→ Pre-generated as HTML"]
end
subgraph RequestTime["At Request Time"]
Step1["1. Return static shell immediately (minimize TTFB)"]
Step2["2. Stream dynamic content"]
Step3["3. Progressive hydration at Suspense boundaries"]
Step1 --> Step2 --> Step3
end
BuildTime --> RequestTime
next/after API
Execute Processing After Response
next/after is a new API that executes processing after sending the response to the client.
// app/api/submit/route.ts
import { after } from 'next/server';
export async function POST(request: Request) {
const data = await request.json();
// Return response immediately
const result = await processSubmission(data);
// Background processing after response
after(async () => {
await sendEmailNotification(data.email);
await logAnalytics('form_submitted', data);
await updateSearchIndex(result.id);
});
return Response.json({ success: true, id: result.id });
}
// Usage in Server Component
import { after } from 'next/server';
export default async function Page() {
const data = await fetchData();
// Execute after page rendering
after(() => {
logPageView('/dashboard');
});
return <Dashboard data={data} />;
}
Use Cases
- Analytics submission
- Logging
- Cache warming
- Sending notifications
- Search index updates
Cache Behavior Changes
Default Behavior Changes (Breaking Change)
In Next.js 15, the default caching behavior has significantly changed.
// Before Next.js 14
fetch('https://api.example.com/data');
// Default: force-cache (cached)
// Next.js 15
fetch('https://api.example.com/data');
// Default: no-store (not cached)
Explicit Cache Specification
// Explicitly specify to enable caching
fetch('https://api.example.com/data', {
cache: 'force-cache',
});
// Or specify revalidate
fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // 1 hour
});
Route Handler Caching
// app/api/data/route.ts
// Next.js 14: GET was cached by default
// Next.js 15: No cache by default
export async function GET() {
const data = await fetchData();
return Response.json(data);
}
// To make it static
export const dynamic = 'force-static';
// Or specify revalidate
export const revalidate = 3600;
Client Router Cache
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
staleTimes: {
dynamic: 30, // Cache time for dynamic pages (seconds)
static: 180, // Cache time for static pages (seconds)
},
},
};
Instrumentation API Stabilization
Application Bootstrap
// instrumentation.ts (project root)
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Execute only in Node.js runtime
const { initializeDatabase } = await import('./lib/db');
await initializeDatabase();
const { startMetricsCollection } = await import('./lib/metrics');
startMetricsCollection();
}
if (process.env.NEXT_RUNTIME === 'edge') {
// Initialization in Edge runtime
console.log('Edge runtime initialized');
}
}
export async function onRequestError(
err: Error,
request: Request,
context: { routerKind: string; routePath: string }
) {
// Error tracking
await reportError(err, {
url: request.url,
...context,
});
}
Developer Experience Improvements
Improved Error Display
flowchart TB
subgraph ErrorUI["Next.js 15 Error UI Improvements"]
Error["Error: Cannot read properties of undefined"]
subgraph SourceCode["Source code"]
Line12["12 │ const user = await getUser(id);"]
Line13["13 │ return user.name; ← Error here"]
Line14["14 │ }"]
end
subgraph StackTrace["Stack trace"]
ST1["app/dashboard/page.tsx:13"]
ST2["..."]
end
Actions["[View docs] [Report issue]"]
Error --> SourceCode --> StackTrace --> Actions
end
Static Indicator
Visually confirm static/dynamic rendering during development.
// Static pages show green indicator
// Dynamic pages show blue indicator
// displayed in bottom right of browser
Upgrade Method
Automatic Upgrade
# Automatic upgrade using codemod
npx @next/codemod@canary upgrade latest
Manual Upgrade
# Update dependencies
npm install next@latest react@rc react-dom@rc
# For TypeScript
npm install @types/react@rc @types/react-dom@rc
Main Migration Tasks
// 1. fetch cache behavior
// Before (Next.js 14)
fetch('/api/data'); // Cached
// After (Next.js 15)
fetch('/api/data', { cache: 'force-cache' }); // Explicitly specify
// 2. Route Handler
// Before
export async function GET() { ... } // Auto-cached
// After
export const dynamic = 'force-static'; // Explicitly make static
export async function GET() { ... }
// 3. Server Actions import
// Before
'use server';
// Entire file is Server Actions
// After (recommended)
// Separate into actions.ts and import
import { submitForm } from './actions';
Recommended New Project Structure
my-app/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ └── signup/
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api/
│ │ └── [...route]/
│ ├── actions/ # Server Actions
│ │ └── form.ts
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ui/
│ └── features/
├── lib/
│ ├── db.ts
│ └── utils.ts
├── instrumentation.ts # New addition
├── next.config.ts # From .mjs to .ts
└── package.json
Performance Optimization Tips
1. Leverage Turbopack
// package.json
{
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start"
}
}
2. Gradual PPR Adoption
// Enable sequentially starting with high-traffic pages
export const experimental_ppr = true;
3. Appropriate Caching Strategy
// Cache settings based on data characteristics
const staticData = await fetch('/api/config', {
cache: 'force-cache',
});
const userData = await fetch('/api/user', {
cache: 'no-store', // Always fresh
});
const productData = await fetch('/api/products', {
next: { revalidate: 60 }, // Update every minute
});
Summary
Next.js 15 represents significant evolution in both performance and developer experience.
Key Upgrade Points
- React 19 Support: New hooks, improved Server Actions
- Turbopack Stable: Dramatically faster build times during development
- PPR: Optimal combination of static and dynamic
- Cache Changes: More predictable default behavior
Migration Notes
- fetch cache behavior defaults to no-store
- Route Handler caching changed similarly
- Recommend gradual migration using codemods
Combined with React 19, Next.js has become even more powerful as a full-stack React framework.