Performance
Next.js 16 introduces significant performance improvements with React Compiler, better Server Components support, and new caching strategies. This guide covers best practices to maximize your application's performance.
In Next.js 16, all components are Server Components by default. They run on the server, reduce client bundle size, and improve initial performance. Only use "use client" when you need interactivity.
// Server Component (default in Next.js 16)// Runs on the server, not sent to clientimport { db } from '@/lib/db';export default async function ProductsPage() { // This function runs on the server const products = await db.products.findMany(); return ( <div> <h1>Products</h1> <ProductsList products={products} /> </div> );}// Child component is also Server Component by defaultasync function ProductsList({ products }) { return ( <ul> {products.map(product => ( <li key={product.id}> <h2>{product.name}</h2> <p>{product.description}</p> </li> ))} </ul> );}// Server Component (default in Next.js 16)// Runs on the server, not sent to clientimport { db } from '@/lib/db';export default async function ProductsPage() { // This function runs on the server const products = await db.products.findMany(); return ( <div> <h1>Products</h1> <ProductsList products={products} /> </div> );}// Child component is also Server Component by defaultasync function ProductsList({ products }) { return ( <ul> {products.map(product => ( <li key={product.id}> <h2>{product.name}</h2> <p>{product.description}</p> </li> ))} </ul> );}Use Client Components only for interactivity (events, state, effects). Each Client Component increases bundle size. Keep business logic in Server Components whenever possible.
"use client"; // Required for interactivityimport { useState } from 'react';export function InteractiveButton() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)} className="px-4 py-2 bg-primary text-primary-foreground rounded-md" > Clicks: {count} </button> );}// DON'T use "use client" if you don't need interactivity// Server Components are faster and reduce bundle size"use client"; // Required for interactivityimport { useState } from 'react';export function InteractiveButton() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)} className="px-4 py-2 bg-primary text-primary-foreground rounded-md" > Clicks: {count} </button> );}// DON'T use "use client" if you don't need interactivity// Server Components are faster and reduce bundle sizeUse dynamic imports to load heavy components only when needed. This reduces the initial bundle and improves Time to Interactive.
import dynamic from 'next/dynamic';import { Suspense } from 'react';// Lazy loading with loading stateconst HeavyChart = dynamic(() => import('./Chart'), { loading: () => <div>Loading chart...</div>, ssr: false, // Disable SSR if not needed});// With named exportsconst HeavyEditor = dynamic( () => import('./Editor').then(mod => ({ default: mod.Editor })), { loading: () => <p>Loading editor...</p> });// With Suspense for better controlexport default function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<div>Loading...</div>}> <HeavyChart /> </Suspense> </div> );}import dynamic from 'next/dynamic';import { Suspense } from 'react';// Lazy loading with loading stateconst HeavyChart = dynamic(() => import('./Chart'), { loading: () => <div>Loading chart...</div>, ssr: false, // Disable SSR if not needed});// With named exportsconst HeavyEditor = dynamic( () => import('./Editor').then(mod => ({ default: mod.Editor })), { loading: () => <p>Loading editor...</p> });// With Suspense for better controlexport default function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<div>Loading...</div>}> <HeavyChart /> </Suspense> </div> );}The React Compiler automatically optimizes your components, memoizing values and callbacks without needing useMemo or useCallback. Available in Next.js 16+.
import type { NextConfig } from "next";const nextConfig: NextConfig = { // Enable React Compiler (Next.js 16+) reactCompiler: true, // Or advanced configuration: // reactCompiler: { // compilationMode: 'annotation', // or 'infer' // },};export default nextConfig;import type { NextConfig } from "next";const nextConfig: NextConfig = { // Enable React Compiler (Next.js 16+) reactCompiler: true, // Or advanced configuration: // reactCompiler: { // compilationMode: 'annotation', // or 'infer' // },};export default nextConfig;Next.js 16 offers multiple cache levels: React cache for deduplication, Next.js cache for data, and on-demand revalidation with tags.
import { cache } from 'react';import { unstable_cache } from 'next/cache';// React cache for request deduplicationconst getProduct = cache(async (id) => { const res = await fetch(`https://api.example.com/products/${id}`); return res.json();});// Next.js cache with revalidationconst getCachedProducts = unstable_cache( async () => { const res = await fetch('https://api.example.com/products'); return res.json(); }, ['products'], // cache key { revalidate: 3600, // Revalidate every hour tags: ['products'], // For on-demand revalidation });export default async function ProductsPage() { // Multiple calls to getProduct with same id // only make one real request (deduplication) const product1 = await getProduct(1); const product2 = await getProduct(1); // Uses cache const products = await getCachedProducts(); return <div>{/* ... */}</div>;}import { cache } from 'react';import { unstable_cache } from 'next/cache';// React cache for request deduplicationconst getProduct = cache(async (id) => { const res = await fetch(`https://api.example.com/products/${id}`); return res.json();});// Next.js cache with revalidationconst getCachedProducts = unstable_cache( async () => { const res = await fetch('https://api.example.com/products'); return res.json(); }, ['products'], // cache key { revalidate: 3600, // Revalidate every hour tags: ['products'], // For on-demand revalidation });export default async function ProductsPage() { // Multiple calls to getProduct with same id // only make one real request (deduplication) const product1 = await getProduct(1); const product2 = await getProduct(1); // Uses cache const products = await getCachedProducts(); return <div>{/* ... */}</div>;}Next.js automatically caches fetch responses. Configure time-based revalidation (ISR) or tag-based revalidation to keep data fresh.
// Fetch with automatic cachingexport default async function DataPage() { // Default cache (force-cache) const staticData = await fetch('https://api.example.com/data', { cache: 'force-cache', // Indefinite cache }); // Revalidation every 60 seconds const revalidatedData = await fetch('https://api.example.com/data', { next: { revalidate: 60 }, }); // Revalidation with tags const taggedData = await fetch('https://api.example.com/data', { next: { revalidate: 3600, tags: ['data'], }, }); // No cache (always fresh) const freshData = await fetch('https://api.example.com/data', { cache: 'no-store', }); // Cache only at build time const buildTimeData = await fetch('https://api.example.com/data', { next: { revalidate: false }, }); const data = await staticData.json(); return <div>{/* ... */}</div>;}// Fetch with automatic cachingexport default async function DataPage() { // Default cache (force-cache) const staticData = await fetch('https://api.example.com/data', { cache: 'force-cache', // Indefinite cache }); // Revalidation every 60 seconds const revalidatedData = await fetch('https://api.example.com/data', { next: { revalidate: 60 }, }); // Revalidation with tags const taggedData = await fetch('https://api.example.com/data', { next: { revalidate: 3600, tags: ['data'], }, }); // No cache (always fresh) const freshData = await fetch('https://api.example.com/data', { cache: 'no-store', }); // Cache only at build time const buildTimeData = await fetch('https://api.example.com/data', { next: { revalidate: false }, }); const data = await staticData.json(); return <div>{/* ... */}</div>;}Next.js 16 recommends using separate loading.jsx files alongside page.jsx. This automatically wraps your page in a Suspense boundary, improving Time to First Byte (TTFB) and allowing users to see content while other parts load.
import { Suspense } from 'react';// Page component with async dataexport default async function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* Renders immediately */} <Header /> {/* Renders when data is ready */} <Suspense fallback={<DashboardSkeleton />}> <DashboardContent /> </Suspense> {/* Another independent block */} <Suspense fallback={<StatsSkeleton />}> <Stats /> </Suspense> </div> );}async function DashboardContent() { // This call might be slow const data = await fetch('https://api.example.com/dashboard', { cache: 'no-store', }).then(r => r.json()); return <div>{/* ... */}</div>;}async function Stats() { const stats = await fetch('https://api.example.com/stats', { cache: 'no-store', }).then(r => r.json()); return <div>{/* ... */}</div>;}import { Suspense } from 'react';// Page component with async dataexport default async function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* Renders immediately */} <Header /> {/* Renders when data is ready */} <Suspense fallback={<DashboardSkeleton />}> <DashboardContent /> </Suspense> {/* Another independent block */} <Suspense fallback={<StatsSkeleton />}> <Stats /> </Suspense> </div> );}async function DashboardContent() { // This call might be slow const data = await fetch('https://api.example.com/dashboard', { cache: 'no-store', }).then(r => r.json()); return <div>{/* ... */}</div>;}async function Stats() { const stats = await fetch('https://api.example.com/stats', { cache: 'no-store', }).then(r => r.json()); return <div>{/* ... */}</div>;}Next.js Image component automatically optimizes images, serving modern formats (WebP/AVIF), lazy loading, and responsive images. Significantly reduces image sizes.
import Image from 'next/image';// Automatic image optimizationexport function ProductImage({ src, alt, width, height }) { return ( <Image src={src} alt={alt} width={width} height={height} // Lazy loading by default loading="lazy" // Placeholder while loading placeholder="blur" blurDataURL="data:image/jpeg;base64,..." // Priority for above-the-fold images priority={false} // Modern formats automatically // Next.js serves WebP/AVIF automatically // Responsive images sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> );}// For above-the-fold imagesexport function HeroImage({ src, alt }) { return ( <Image src={src} alt={alt} width={1920} height={1080} priority // Load immediately quality={90} /> );}import Image from 'next/image';// Automatic image optimizationexport function ProductImage({ src, alt, width, height }) { return ( <Image src={src} alt={alt} width={width} height={height} // Lazy loading by default loading="lazy" // Placeholder while loading placeholder="blur" blurDataURL="data:image/jpeg;base64,..." // Priority for above-the-fold images priority={false} // Modern formats automatically // Next.js serves WebP/AVIF automatically // Responsive images sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> );}// For above-the-fold imagesexport function HeroImage({ src, alt }) { return ( <Image src={src} alt={alt} width={1920} height={1080} priority // Load immediately quality={90} /> );}Reduce bundle sizes by importing only what's needed, using tree shaking, and analyzing bundle sizes with tools like bundle-analyzer.
// Imports only what you needimport { Heart, Star, Share } from 'lucide-react';export function IconButton({ icon }) { const Icon = icon === 'heart' ? Heart : Star; return <Icon className="w-5 h-5" />;}// For large libraries, use dynamic importsconst HeavyLibrary = dynamic(() => import('heavy-library'));// Imports only what you needimport { Heart, Star, Share } from 'lucide-react';export function IconButton({ icon }) { const Icon = icon === 'heart' ? Heart : Star; return <Icon className="w-5 h-5" />;}// For large libraries, use dynamic importsconst HeavyLibrary = dynamic(() => import('heavy-library'));Partial Prerendering combines the best of SSG and SSR: prerenders static parts and streams dynamic parts. Improves initial performance while maintaining dynamic content.
import { Suspense } from 'react';// Partial Prerendering (Next.js 16+)// Renders static part immediately// and streams dynamic partexport default function ProductPage({ params }) { return ( <div> {/* This part is prerendered statically */} <ProductHeader /> <ProductDescription /> {/* This part is streamed dynamically */} <Suspense fallback={<ReviewsSkeleton />}> <ProductReviews productId={params.id} /> </Suspense> <Suspense fallback={<RelatedSkeleton />}> <RelatedProducts productId={params.id} /> </Suspense> </div> );}// Static components (prerendered)function ProductHeader() { return <header>{/* ... */}</header>;}// Dynamic components (streamed)async function ProductReviews({ productId }) { const reviews = await fetch( `https://api.example.com/products/${productId}/reviews`, { cache: 'no-store' } ).then(r => r.json()); return <div>{/* ... */}</div>;}import { Suspense } from 'react';// Partial Prerendering (Next.js 16+)// Renders static part immediately// and streams dynamic partexport default function ProductPage({ params }) { return ( <div> {/* This part is prerendered statically */} <ProductHeader /> <ProductDescription /> {/* This part is streamed dynamically */} <Suspense fallback={<ReviewsSkeleton />}> <ProductReviews productId={params.id} /> </Suspense> <Suspense fallback={<RelatedSkeleton />}> <RelatedProducts productId={params.id} /> </Suspense> </div> );}// Static components (prerendered)function ProductHeader() { return <header>{/* ... */}</header>;}// Dynamic components (streamed)async function ProductReviews({ productId }) { const reviews = await fetch( `https://api.example.com/products/${productId}/reviews`, { cache: 'no-store' } ).then(r => r.json()); return <div>{/* ... */}</div>;}Best Practices
• Reduces client bundle size
• Improves initial performance
• Direct access to APIs and databases
• Better SEO and load time
• React cache for deduplication
• Next.js cache with revalidation
• Tags for on-demand revalidation
• Configure appropriate revalidation times
• Automatic Suspense wrapping
• Shows immediately while page loads
• Better UX than manual fallbacks
• Works with streaming and PPR
• Import only what's needed
• Use dynamic imports for heavy code
• Analyze bundles regularly
• Leverage tree shaking