Discover the latest features in Next.js 15 and learn how to smoothly migrate your existing applications to take advantage of improved performance and developer experience.
Next.js 15 brings significant improvements in performance, developer experience, and new features that make building full-stack React applications even more enjoyable. Let's explore what's new and how you can migrate your existing projects.
The App Router, introduced in Next.js 13, receives major stability and performance improvements:
Turbopack, the Rust-based bundler, is now stable for development:
# Enable Turbopack for development
npm run dev -- --turbo
# Or add to package.json
{
"scripts": {
"dev": "next dev --turbo"
}
}
Performance improvements:
Next.js 15 fully supports React 19 features:
// Server Components with async/await
async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUser(userId);
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
// New use() hook for data fetching
function UserPosts({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return (
<div>
{user.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Revolutionary rendering strategy that combines static and dynamic content:
// app/product/[id]/page.tsx
import { Suspense } from 'react';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* Static: Prerendered at build time */}
<ProductHeader productId={params.id} />
{/* Dynamic: Rendered on demand */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
{/* Dynamic: User-specific content */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
</div>
);
}
// Enable PPR
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
Enhanced caching strategies with better control:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await fetchPosts();
return NextResponse.json(posts, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
});
}
// Fine-grained cache control
export const revalidate = 3600; // Revalidate every hour
export const dynamic = 'force-static'; // Force static generation
Enhanced Server Actions with better type safety and error handling:
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
// app/create-post/page.tsx
import { createPost } from "../actions";
// app/actions.ts
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
try {
const post = await db.post.create({
data: { title, content },
});
revalidatePath("/blog");
redirect(`/blog/${post.slug}`);
} catch (error) {
return { error: "Failed to create post" };
}
}
export default function CreatePost() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Post content" required />
<button type="submit">Create Post</button>
</form>
);
}
npm install next@15 react@19 react-dom@19
npm install @types/react@19 @types/react-dom@19 # If using TypeScript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable new features
experimental: {
ppr: true, // Partial Prerendering
reactCompiler: true, // React Compiler
},
// Remove deprecated options
// swcMinify: true, // Now default, remove this line
};
module.exports = nextConfig;
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
If you're still using the Pages Router, consider migrating to App Router:
// pages/blog/[slug].tsx (Old)
export default function BlogPost({ post }) {
return <div>{post.title}</div>;
}
export async function getStaticProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post } };
}
// app/blog/[slug]/page.tsx (New)
interface PageProps {
params: { slug: string };
}
export default async function BlogPost({ params }: PageProps) {
const post = await fetchPost(params.slug);
return <div>{post.title}</div>;
}
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/global-error.tsx
('use client');
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
// Use the new Image component optimizations
import Image from 'next/image';
export default function Gallery() {
return (
<div>
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // Load above the fold images first
placeholder="blur" // Show blur while loading
blurDataURL="data:image/jpeg;base64,..." // Blur placeholder
/>
</div>
);
}
// ❌ Removed in Next.js 15
import { useRouter } from 'next/router'; // Pages Router only
// ✅ Use App Router navigation
import { useRouter } from 'next/navigation';
// ❌ Old behavior: fetch requests were cached by default
fetch('/api/data'); // Was cached
// ✅ New behavior: opt-in caching
fetch('/api/data', { cache: 'force-cache' }); // Explicitly cached
fetch('/api/data', { next: { revalidate: 3600 } }); // Revalidated
# Analyze bundle size
npm install -g @next/bundle-analyzer
ANALYZE=true npm run build
// Dynamic imports with better loading states
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <ComponentSkeleton />,
ssr: false, // Disable SSR for client-only components
});
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</div>
);
}
# Lighthouse CI
npm install -g @lhci/cli
lhci autorun
# Core Web Vitals monitoring
npm install web-vitals
# Before migration
npm run build
npm run analyze
# After migration (compare results)
npm run build
npm run analyze
Next.js 15 represents a significant leap forward in React framework capabilities. The improved performance, better developer experience, and new features like Partial Prerendering make it a compelling upgrade for any React application.
Migration checklist: