Command Palette

Search for a command to run...

Performance

Complete guide to optimize Next.js 16 applications using Server Components, React Compiler, Cache, and more.

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>  );}

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 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>  );}

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;

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>;}

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>;}

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>;}

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}    />  );}

Reduce bundle sizes by importing only what's needed, using tree shaking, and analyzing bundle sizes with tools like bundle-analyzer.

// ❌ Imports entire packageimport * as Icons from 'lucide-react';// ✅ Import 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>;}

Best Practices

1. Prioritize Server Components
Use Server Components by default and Client Components only when you need interactivity

• Reduces client bundle size

• Improves initial performance

• Direct access to APIs and databases

• Better SEO and load time

2. Use Cache Strategically
Leverage React cache and Next.js cache to reduce redundant requests

• React cache for deduplication

• Next.js cache with revalidation

• Tags for on-demand revalidation

• Configure appropriate revalidation times

3. Implement Loading Files
Use loading.jsx files alongside page.jsx for automatic Suspense boundaries and better TTFB

• Automatic Suspense wrapping

• Shows immediately while page loads

• Better UX than manual fallbacks

• Works with streaming and PPR

4. Optimize Bundles
Reduce bundle sizes with selective imports and code splitting

• Import only what's needed

• Use dynamic imports for heavy code

• Analyze bundles regularly

• Leverage tree shaking