Next.js Performance Optimization Playbook
A systems-oriented handbook for rendering, data fetching, bundling, and animation performance in modern Next.js apps.
Modern Next.js applications demand peak performance across rendering, data fetching, bundling, and animation. This playbook provides battle-tested optimization strategies for production-scale applications.
Rendering & Hydration
Server Components by Default
Maximize Server Components usage to reduce client bundle size. Move interactive logic to leaf components only.
// ✅ Good: Server Component wrapper
export default async function ProductPage() {
const products = await getProducts();
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
<AddToCartButton /> {/* Only this needs 'use client' */}
</div>
);
}
Selective Hydration with Dynamic Imports
Defer non-critical component hydration using dynamic imports with SSR disabled for heavy components.
const HeavyChart = dynamic(
() => import('./HeavyChart'),
{
ssr: false,
loading: () => <ChartSkeleton />
}
);
Streaming SSR with Suspense
Implement progressive rendering to improve Time to First Byte (TTFB) and perceived performance.
export default function Layout({ children }) {
return (
<div>
<Header /> {/* Renders immediately */}
<Suspense fallback={<NavSkeleton />}>
<Navigation /> {/* Streams when ready */}
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
{children} {/* Streams main content */}
</Suspense>
</div>
);
}
Data Fetching Optimization
Request Deduplication
Next.js automatically deduplicates fetch requests. Use unstable_cache for non-fetch operations.
import { unstable_cache } from 'next/cache';
const getUser = unstable_cache(
async (userId: string) => {
return db.user.findUnique({ where: { id: userId } });
},
['user-cache'],
{
tags: ['user'],
revalidate: 3600, // 1 hour
}
);
Parallel Data Loading
Fetch data in parallel rather than waterfall sequences to reduce overall loading time.
// ✅ Good: Parallel fetching
export default async function Dashboard() {
// Start all requests simultaneously
const [user, posts, analytics] = await Promise.all([
getUser(),
getPosts(),
getAnalytics(),
]);
return <DashboardContent {...{ user, posts, analytics }} />;
}
Optimistic Updates
Implement optimistic UI updates for better perceived performance in client components.
'use client';
import { useOptimistic } from 'react';
export function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
async function handleAdd(todo) {
addOptimisticTodo(todo);
await addTodo(todo);
}
return <>{/* Render optimisticTodos */}</>;
}
Bundle Size Optimization
Tree-Shaking & Code Splitting
Use ES6 imports and dynamic imports to ensure effective tree-shaking and code splitting.
// ✅ Named imports for tree-shaking
import { format } from 'date-fns';
// ✅ Dynamic imports for code splitting
const PDFViewer = dynamic(() =>
import('components/PDFViewer').then(mod => mod.PDFViewer)
);
Bundle Analysis
Use @next/bundle-analyzer to identify and eliminate large dependencies.
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Your config
});
Optimize Third-Party Scripts
Use Next.js Script component with appropriate loading strategies.
import Script from 'next/script';
export default function App() {
return (
<>
{/* Critical scripts */}
<Script src="critical.js" strategy="beforeInteractive" />
{/* Non-critical scripts */}
<Script src="analytics.js" strategy="lazyOnload" />
{/* Scripts that need page interaction */}
<Script src="chat-widget.js" strategy="afterInteractive" />
</>
);
}
Animation Performance
CSS Transform & Will-Change
Use GPU-accelerated properties and hint browser optimizations.
/* ✅ GPU-accelerated animations */
.animated-element {
will-change: transform, opacity;
transform: translateZ(0); /* Force GPU layer */
}
@keyframes slideIn {
from { transform: translate3d(-100%, 0, 0); }
to { transform: translate3d(0, 0, 0); }
}
Framer Motion Optimization
Use layout animations sparingly and leverage MotionConfig for global settings.
import { MotionConfig, motion, LazyMotion, domAnimation } from 'framer-motion';
// Reduce bundle size with feature selection
export function App() {
return (
<LazyMotion features={domAnimation} strict>
<MotionConfig reducedMotion="user">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
</MotionConfig>
</LazyMotion>
);
}
Intersection Observer for Animations
Trigger animations only when elements are visible to improve initial load performance.
'use client';
import { useInView } from 'react-intersection-observer';
export function AnimatedSection({ children }) {
const { ref, inView } = useInView({
triggerOnce: true,
threshold: 0.1,
});
return (
<div
ref={ref}
className={inView ? 'animate-fade-in' : 'opacity-0'}
>
{children}
</div>
);
}
Image & Media Optimization
Next.js Image Component
Always use next/image with proper sizing and priority hints.
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority // LCP image
placeholder="blur"
blurDataURL={blurDataUrl}
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}
Responsive Images
Serve appropriately sized images for different viewports.
<Image
src="/product.jpg"
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw"
style={{
width: '100%',
height: 'auto',
}}
/>
Caching Strategies
Next.js 15 Caching
Leverage the new caching directives for fine-grained control.
import { unstable_cache } from 'next/cache';
// Page-level caching
export const revalidate = 3600; // 1 hour
// Component-level caching
const getCachedData = unstable_cache(
async () => fetchExpensiveData(),
['data-cache'],
{
revalidate: 60,
tags: ['data'],
}
);
Static Generation with ISR
Combine static generation with incremental updates for optimal performance.
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({
slug: post.slug,
}));
}
export const revalidate = 3600; // Revalidate every hour
Performance Monitoring
Web Vitals Tracking
Monitor Core Web Vitals in production to identify performance regressions.
// app/layout.tsx
import { WebVitals } from './web-vitals';
export default function Layout({ children }) {
return (
<html>
<body>
<WebVitals />
{children}
</body>
</html>
);
}
Custom Performance Marks
Add custom performance measurements for critical user journeys.
'use client';
export function CheckoutFlow() {
useEffect(() => {
performance.mark('checkout-start');
return () => {
performance.mark('checkout-end');
performance.measure(
'checkout-duration',
'checkout-start',
'checkout-end'
);
};
}, []);
}
Advanced Techniques
Resource Hints
Use DNS prefetch, preconnect, and prefetch for critical resources.
export default function Layout() {
return (
<html>
<head>
<link rel="dns-prefetch" href="https://api.example.com" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="prefetch" href="/critical-data.json" />
</head>
</html>
);
}
Edge Runtime Optimization
Use Edge Runtime for lightweight API routes and middleware.
export const runtime = 'edge';
export async function GET(request: Request) {
// Lightweight processing at the edge
const data = await fetch('https://api.example.com/data');
return new Response(JSON.stringify(data), {
headers: {
'content-type': 'application/json',
'cache-control': 'public, s-maxage=60',
},
});
}
Partial Prerendering (Experimental)
Combine static and dynamic rendering for optimal performance.
// next.config.js
module.exports = {
experimental: {
ppr: true,
},
};
// In your page
export const experimental_ppr = true;
Quick Reference Checklist
- ✅ Use Server Components by default, Client Components only when needed
- ✅ Implement streaming SSR with Suspense boundaries
- ✅ Parallelize data fetching with Promise.all()
- ✅ Dynamic import heavy components
- ✅ Optimize images with next/image and proper sizing
- ✅ Use CSS transforms for animations
- ✅ Implement proper caching strategies
- ✅ Monitor Web Vitals in production
- ✅ Analyze and optimize bundle size regularly
- ✅ Use Edge Runtime for lightweight operations
Remember
Performance optimization is an iterative process. Measure first, optimize based on data, and always test the impact of your changes. Focus on the metrics that matter most to your users.