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.