10 Next.js Performance Tips for Production Apps
Performance isn't optional—it's a feature. Users expect fast, responsive web applications, and search engines reward speed with better rankings. Next.js provides powerful tools for optimization, but knowing how to use them effectively is key.
This guide covers 10 battle-tested tips that will make your Next.js applications blazing fast in production.
1. Optimize Images with Next.js Image Component
The next/image component is one of Next.js's most powerful features for performance. It automatically optimizes images on-demand, serving them in modern formats like WebP when supported.
import Image from 'next/image'; export function Hero() { return ( <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} priority // Load immediately for LCP placeholder="blur" blurDataURL="data:image/jpeg;base64,..." sizes="(max-width: 768px) 100vw, 50vw" /> ); }
Key features:
- Automatic WebP conversion - Smaller file sizes without quality loss
- Lazy loading - Images load only when entering viewport
- Priority loading - Critical images load immediately
- Responsive sizing - Serve appropriate sizes for each device
Best practices:
- Always specify
widthandheightto prevent layout shift - Use
priorityfor above-the-fold images - Provide
blurDataURLfor a smooth loading experience - Use
sizesfor responsive images
2. Implement Dynamic Imports for Code Splitting
Code splitting allows you to load JavaScript on-demand, reducing initial bundle size.
import dynamic from 'next/dynamic'; // Dynamic import with loading state const HeavyChart = dynamic( () => import('../components/HeavyChart'), { loading: () => <p>Loading chart...</p>, ssr: false, // Disable SSR for browser-only libraries } ); export function Dashboard() { return ( <div> <h1>Analytics Dashboard</h1> <HeavyChart data={chartData} /> </div> ); }
Use cases:
- Heavy charting libraries (D3, Chart.js)
- Map components (Leaflet, Mapbox)
- Rich text editors
- Modal dialogs and popovers
3. Leverage Streaming and Suspense Boundaries
Next.js 13+ supports React Suspense for streaming, allowing you to progressively render UI.
import { Suspense } from 'react'; async function RelatedProducts() { const products = await fetchRelatedProducts(); return <ProductGrid products={products} />; } export default function ProductPage() { return ( <div> <h1>Product Details</h1> {/* This loads immediately */} <ProductInfo /> {/* This streams in when ready */} <Suspense fallback={<ProductSkeleton />}> <RelatedProducts /> </Suspense> </div> ); }
Benefits:
- Faster Time to First Byte (TTFB)
- Progressive loading experience
- Better perceived performance
4. Implement Proper Caching Strategies
Next.js provides granular control over data fetching and caching.
| Strategy | Use Case | TTL |
|---|---|---|
force-cache | Static data | Forever |
revalidate: 3600 | Semi-static | 1 hour |
revalidate: 60 | Frequently updated | 1 minute |
no-store | Real-time data | Never cache |
// Static generation with revalidation async function getProducts() { const res = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } // Revalidate every hour }); return res.json(); } // Real-time data (no caching) async function getStockPrice() { const res = await fetch('https://api.example.com/stock', { cache: 'no-store' }); return res.json(); } // On-demand revalidation export async function POST() { revalidatePath('/products'); return Response.json({ revalidated: true }); }
5. Minimize Client Components
Every 'use client' directive adds to your JavaScript bundle. Keep client components small and focused.
// Good: Keep server component as parent export default function ProductPage() { const product = await getProduct(); return ( <div> <h1>{product.name}</h1> {/* Only interactivity in client component */} <AddToCartButton productId={product.id} /> </div> ); } // Client component isolated 'use client'; function AddToCartButton({ productId }: { productId: string }) { const [isPending, startTransition] = useTransition(); return ( <button onClick={() => startTransition(() => addToCart(productId))} disabled={isPending} > {isPending ? 'Adding...' : 'Add to Cart'} </button> ); }
6. Analyze and Optimize Your Bundle
Understanding your bundle is crucial for optimization.
# Install analyzer npm install @next/bundle-analyzer # Run analysis ANALYZE=true npm run build
Analyze output:
- Look for duplicate dependencies
- Identify large libraries
- Check for unused code
// next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); module.exports = withBundleAnalyzer({ // Your Next.js config });
7. Optimize Fonts with Next.js Font System
The next/font system automatically optimizes fonts for performance.
import { Inter, Roboto_Mono } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', // Use font-display: swap variable: '--font-inter', }); const robotoMono = Roboto_Mono({ subsets: ['latin'], variable: '--font-roboto-mono', }); export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}> <body className={inter.className}>{children}</body> </html> ); }
Benefits:
- Zero layout shift (no invisible text during loading)
- Automatic self-hosting
- CSS variables for font families
8. Use Edge Runtime for Global Performance
The Edge Runtime provides low-latency responses by running code close to users.
// app/api/search/route.ts export const runtime = 'edge'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const query = searchParams.get('q'); // Fast response from edge locations const results = await search(query); return Response.json(results); }
When to use Edge:
- Geolocation-based content
- Authentication checks
- A/B testing
- Real-time data APIs
9. Implement Partial Prerendering (PPR)
Next.js 14+ introduces Partial Prerendering, combining static and dynamic content.
// app/page.tsx export const experimental_ppr = true; async function Weather() { const weather = await getWeather(); // Dynamic return <WeatherWidget data={weather} />; } export default function HomePage() { return ( <div> {/* Static prerendered shell */} <Header /> <Sidebar /> {/* Dynamic content streams in */} <Suspense fallback={<WeatherSkeleton />}> <Weather /> </Suspense> </div> ); }
10. Monitor Real User Performance
Use Next.js analytics and web vitals to track actual user experience.
// app/_components/web-vitals.tsx 'use client'; import { useReportWebVitals } from 'next/web-vitals'; export function WebVitals() { useReportWebVitals((metric) => { // Send to your analytics console.log(metric); // Example: Send to analytics service analytics.track('Web Vitals', { name: metric.name, value: metric.value, id: metric.id, }); }); return null; }
Key metrics to track:
- LCP (Largest Contentful Paint) - < 2.5s
- FID (First Input Delay) - < 100ms
- CLS (Cumulative Layout Shift) - < 0.1
- TTFB (Time to First Byte) - < 600ms
- FCP (First Contentful Paint) - < 1.8s
Advanced Optimization Techniques
Script Optimization
import Script from 'next/script'; export default function Page() { return ( <> {/* Load after page becomes interactive */} <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" /> {/* Lazy load when idle */} <Script src="https://chat-widget.example.com/widget.js" strategy="lazyOnload" /> {/* Load before page becomes interactive */} <Script src="https://critical-cdn.example.com/lib.js" strategy="beforeInteractive" /> </> ); }
Link Prefetching
import Link from 'next/link'; export function Navigation() { return ( <nav> {/* Default: Prefetch on hover */} <Link href="/about">About</Link> {/* Disable prefetching */} <Link href="/contact" prefetch={false}>Contact</Link> {/* Prefetch immediately on viewport entry */} <Link href="/products" prefetch={true}>Products</Link> </nav> ); }
Performance Checklist
Before deploying to production:
- Enable image optimization with
next/image - Implement proper caching strategies
- Minimize client-side JavaScript
- Use dynamic imports for heavy components
- Optimize fonts with
next/font - Analyze bundle size with
@next/bundle-analyzer - Enable gzip/Brotli compression on server
- Implement proper error boundaries
- Test on slow networks and low-end devices
- Monitor Core Web Vitals in production
Testing Performance
Lighthouse CI
# .github/workflows/lighthouse.yml name: Lighthouse CI on: [push] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run Lighthouse CI run: | npm install -g @lhci/cli@0.12.x lhci autorun
Local Testing
# Build for production npm run build # Start production server npm start # Run Lighthouse audit npx lighthouse http://localhost:3000 --view # Test with slow network # Chrome DevTools > Network > Slow 3G
Conclusion
Performance optimization is an ongoing process, not a one-time task. Start with these fundamentals:
- Image optimization - Biggest impact on page weight
- Code splitting - Load only what's needed
- Caching - Reduce redundant work
- Monitoring - Measure real user experience
Remember: The fastest code is the code you don't send. Use server components, lazy loading, and strategic caching to deliver lightning-fast Next.js applications.
Start with the quick wins—image optimization and code splitting—then iterate based on real user metrics and business goals.