Asia/Dhaka
Posts

Optimizing Next.js Applications for Maximum Performance

October 8, 2024
In today's competitive digital landscape, every millisecond counts. A fast-loading application directly impacts user engagement, conversion rates, and SEO rankings. Next.js provides powerful built-in optimizations, but knowing how to leverage them is key. Real-world statistics:
  • 53% of mobile users abandon sites that take longer than 3 seconds to load
  • 100ms improvement in load time can increase conversion rates by 1%
  • 1 second delay decreases customer satisfaction by 16%
  • Sites loading in 2 seconds have an average bounce rate of 9% vs 38% for 5-second loads
E-commerce example:
  • Amazon: Every 100ms of latency costs 1% in sales
  • Walmart: Every 1 second improvement resulted in 2% increase in conversions
Modern users expect instant experiences. Performance is not just technical—it's a CORE FEATURE that affects:
  • User retention and engagement
  • Conversion and revenue
  • SEO rankings (Google's Page Experience)
  • Brand perception
  • Operating costs (CDN, compute)
Google's Core Web Vitals measure real-world user experience:
  • LCP (Largest Contentful Paint): Loading performance
    • Target: < 2.5 seconds
    • Measures: When main content becomes visible
  • FID (First Input Delay): Interactivity
    • Target: < 100 milliseconds
    • Measures: Time until page responds to first interaction
  • CLS (Cumulative Layout Shift): Visual stability
    • Target: < 0.1
    • Measures: Unexpected layout shifts
Javascript
// pages/_app.js - Track real user metrics
export function reportWebVitals(metric) {
  const { id, name, label, value } = metric;
  
  // Log to console in development
  if (process.env.NODE_ENV === 'development') {
    console.log(name, value);
  }
  
  // Send to analytics in production
  if (label === 'web-vital') {
    // Google Analytics 4
    window.gtag?.('event', name, {
      event_category: label === 'web-vital' ? 'Web Vitals' : 'Next.js Metric',
      value: Math.round(name === 'CLS' ? value * 1000 : value),
      event_label: id,
      non_interaction: true,
    });
    
    // Custom analytics endpoint
    fetch('/api/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        metric: name,
        value: value,
        id: id,
        path: window.location.pathname,
        timestamp: Date.now()
      })
    });
  }
}
Yaml
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            http://localhost:3000/
            http://localhost:3000/blog
            http://localhost:3000/products
          uploadArtifacts: true
          temporaryPublicStorage: true
          runs: 3
Lighthouse CI Configuration:
Javascript
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      startServerCommand: 'npm run start',
      url: ['http://localhost:3000/'],
      numberOfRuns: 3
    },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],
        'categories:best-practices': ['error', { minScore: 0.9 }],
        'categories:seo': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }]
      }
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
};
Javascript
// pages/user/[id].js
export async function getServerSideProps({ params, req, res }) {
  // Set cache headers for edge caching
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=10, stale-while-revalidate=59'
  );
  
  try {
    const userData = await fetchUser(params.id);
    
    if (!userData) {
      return {
        notFound: true,
      };
    }
    
    return {
      props: {
        user: userData,
        fetchedAt: new Date().toISOString()
      }
    };
  } catch (error) {
    return {
      redirect: {
        destination: '/error',
        permanent: false
      }
    };
  }
}

export default function UserPage({ user, fetchedAt }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Data fetched at: {fetchedAt}</p>
    </div>
  );
}
Javascript
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const post = await getPostBySlug(params.slug);
  
  return {
    props: { post },
    // Regenerate page every 60 seconds if there's a request
    revalidate: 60
  };
}

export async function getStaticPaths() {
  // Generate top 100 most popular posts at build time
  const popularPosts = await getPopularPosts(100);
  
  return {
    paths: popularPosts.map(post => ({
      params: { slug: post.slug }
    })),
    // Generate other pages on-demand
    fallback: 'blocking' // or 'true' for instant loading state
  };
}
Jsx
// app/products/[id]/page.jsx
import { Suspense } from 'react';
import ProductDetails from './ProductDetails';
import Reviews from './Reviews';
import RelatedProducts from './RelatedProducts';

// This is a Server Component by default
export default async function ProductPage({ params }) {
  // Fetch in parallel
  const [product, reviews] = await Promise.all([
    fetchProduct(params.id),
    fetchReviews(params.id)
  ]);
  
  return (
    <div>
      {/* Immediate render with fetched data */}
      <ProductDetails product={product} />
      
      {/* Show loading state while fetching related products */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews initialReviews={reviews} />
      </Suspense>
      
      <Suspense fallback={<ProductsSkeleton />}>
        <RelatedProducts productId={params.id} />
      </Suspense>
    </div>
  );
}

// Related products as separate async Server Component
async function RelatedProducts({ productId }) {
  const products = await fetchRelatedProducts(productId);
  return <ProductGrid products={products} />;
}
Jsx
// app/dashboard/page.jsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div className="dashboard">
      {/* Critical content renders first */}
      <Header />
      
      {/* Non-critical content streams in */}
      <Suspense fallback={<Skeleton />}>
        <Analytics />
      </Suspense>
      
      <Suspense fallback={<Skeleton />}>
        <RecentActivity />
      </Suspense>
      
      <Suspense fallback={<Skeleton />}>
        <Chart />
      </Suspense>
    </div>
  );
}

// Each component fetches independently
async function Analytics() {
  const data = await fetchAnalytics(); // Slow query
  return <AnalyticsWidget data={data} />;
}
Performance impact:
  • Time to First Byte (TTFB): Same
  • First Contentful Paint (FCP): 40% faster (renders shell immediately)
  • Largest Contentful Paint (LCP): 25% faster (progressive loading)
Jsx
import Image from 'next/image';

function Hero() {
  return (
    <div className="hero">
      {/* Automatically optimized */}
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1920}
        height={1080}
        priority // Load with high priority (above fold)
        placeholder="blur" // Show blur while loading
        blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // Base64 blur
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        quality={85} // Default is 75
      />
    </div>
  );
}

// Below-the-fold images
function ProductGrid({ products }) {
  return (
    <div className="grid">
      {products.map(product => (
        <Image
          key={product.id}
          src={product.image}
          alt={product.name}
          width={400}
          height={400}
          loading="lazy" // Lazy load (default for non-priority images)
          placeholder="blur"
          blurDataURL={product.blurHash}
        />
      ))}
    </div>
  );
}
Javascript
// scripts/generate-blur.js
import { getPlaiceholder } from 'plaiceholder';
import fs from 'fs';

async function generateBlurData() {
  const images = [
    'public/hero.jpg',
    'public/product1.jpg',
    // ... more images
  ];
  
  const blurData = {};
  
  for (const imagePath of images) {
    const { base64 } = await getPlaiceholder(imagePath);
    blurData[imagePath] = base64;
  }
  
  fs.writeFileSync(
    'blur-data.json',
    JSON.stringify(blurData, null, 2)
  );
}

generateBlurData();
Jsx
import Image from 'next/image';

function ResponsiveHero() {
  return (
    <picture>
      {/* Mobile: Portrait */}
      <source
        media="(max-width: 768px)"
        srcSet="/hero-mobile.jpg"
      />
      
      {/* Tablet: Square */}
      <source
        media="(max-width: 1200px)"
        srcSet="/hero-tablet.jpg"
      />
      
      {/* Desktop: Landscape */}
      <Image
        src="/hero-desktop.jpg"
        alt="Hero"
        width={1920}
        height={1080}
        priority
      />
    </picture>
  );
}
Javascript
// next.config.js
module.exports = {
  images: {
    domains: ['cdn.example.com', 'images.unsplash.com'],
    loader: 'cloudinary', // or 'imgix', 'akamai', 'custom'
    path: 'https://res.cloudinary.com/demo/',
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    formats: ['image/avif', 'image/webp'], // Modern formats
    minimumCacheTTL: 60,
    dangerouslyAllowSVG: true,
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
  }
};
Performance gains:
  • 40-60% smaller file sizes with AVIF/WebP
  • Automatic responsive images
  • Lazy loading by default
  • Prevents layout shift with aspect ratio
Javascript
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

// Heavy charting library - load only when needed
const Chart = dynamic(
  () => import('../components/HeavyChart'),
  { 
    loading: () => <ChartSkeleton />,
    ssr: false // Client-only component
  }
);

// Code-split by route
const AdminPanel = dynamic(
  () => import('../components/AdminPanel'),
  {
    loading: () => <div>Loading admin panel...</div>,
    ssr: true // Can be server-rendered
  }
);

// Conditional loading
function Dashboard({ isAdmin }) {
  return (
    <div>
      <h1>Dashboard</h1>
      {isAdmin && <AdminPanel />}
    </div>
  );
}
Javascript
// Automatic code splitting by route in App Router
app/
├── (marketing)/
│   ├── page.jsx           // Bundle: marketing
│   ├── about/page.jsx     // Bundle: marketing-about
│   └── contact/page.jsx   // Bundle: marketing-contact
├── (shop)/
│   ├── product/[id]/      // Bundle: shop-product
│   └── cart/              // Bundle: shop-cart
└── (dashboard)/
    ├── analytics/         // Bundle: dashboard-analytics
    └── settings/          // Bundle: dashboard-settings

// Each route only loads its required JavaScript
Jsx
'use client';

import { lazy, Suspense, useState } from 'react';

// Lazy load modals
const ShareModal = lazy(() => import('./ShareModal'));
const EditModal = lazy(() => import('./EditModal'));

export default function Article() {
  const [showShare, setShowShare] = useState(false);
  const [showEdit, setShowEdit] = useState(false);
  
  return (
    <div>
      <article>Content here...</article>
      
      <button onClick={() => setShowShare(true)}>
        Share
      </button>
      
      {showShare && (
        <Suspense fallback={<ModalSkeleton />}>
          <ShareModal onClose={() => setShowShare(false)} />
        </Suspense>
      )}
      
      {showEdit && (
        <Suspense fallback={<ModalSkeleton />}>
          <EditModal onClose={() => setShowEdit(false)} />
        </Suspense>
      )}
    </div>
  );
}
Bash
# Install bundle analyzer
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Your Next.js config
});

# Run analysis
ANALYZE=true npm run build
Javascript
// ❌ Bad - imports entire library (100KB+)
import _ from 'lodash';
const debounced = _.debounce(fn, 300);

// ✅ Good - imports only needed function (5KB)
import debounce from 'lodash-es/debounce';
const debounced = debounce(fn, 300);

// ❌ Bad - imports all icons
import { FaHome, FaUser, FaSearch } from 'react-icons/fa';

// ✅ Good - specific imports
import FaHome from 'react-icons/fa/FaHome';
import FaUser from 'react-icons/fa/FaUser';
import FaSearch from 'react-icons/fa/FaSearch';
Bash
# Analyze unused dependencies
npx depcheck

# Remove unused packages
npm uninstall <unused-package>

# Check bundle impact
npm run build
Javascript
// next.config.js
module.exports = {
  swcMinify: true, // Use SWC minifier (faster than Terser)
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
  experimental: {
    modern: true, // Ship modern JavaScript to modern browsers
  }
};
Javascript
// pages/api/edge-function.js
export const config = {
  runtime: 'edge', // Use Edge Runtime for better performance
};

export default async function handler(req) {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get('id');
  
  // Fast response from edge location
  const data = await fetch(`https://api.example.com/data/${id}`);
  
  return new Response(
    JSON.stringify({ data: await data.json() }),
    {
      status: 200,
      headers: {
        'content-type': 'application/json',
        'cache-control': 'public, s-maxage=60, stale-while-revalidate=120'
      },
    }
  );
}
Javascript
// app/api/products/route.js
import { NextResponse } from 'next/server';

export async function GET(request) {
  const products = await fetchProducts();
  
  return NextResponse.json(products, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400'
    }
  });
}

// Enable edge runtime
export const runtime = 'edge';

// Revalidate every hour
export const revalidate = 3600;
Javascript
// lib/prisma.js
import { PrismaClient } from '@prisma/client';

const globalForPrisma = global;

export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
  });

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

// Configure connection pool
// In DATABASE_URL: ?connection_limit=10&pool_timeout=20
Javascript
// ❌ Bad - N+1 queries
async function getPostsWithAuthors() {
  const posts = await prisma.post.findMany();
  
  // Makes a separate query for EACH post
  const postsWithAuthors = await Promise.all(
    posts.map(async post => ({
      ...post,
      author: await prisma.user.findUnique({
        where: { id: post.authorId }
      })
    }))
  );
  
  return postsWithAuthors;
}

// ✅ Good - Single query with join
async function getPostsWithAuthors() {
  const posts = await prisma.post.findMany({
    include: {
      author: true, // Join in single query
      comments: {
        take: 5,
        orderBy: { createdAt: 'desc' }
      }
    }
  });
  
  return posts;
}
Javascript
import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  max: 500,
  ttl: 1000 * 60 * 5, // 5 minutes
});

async function getCachedProducts() {
  const cacheKey = 'products:all';
  
  // Check cache first
  const cached = cache.get(cacheKey);
  if (cached) return cached;
  
  // Fetch from database
  const products = await prisma.product.findMany();
  
  // Store in cache
  cache.set(cacheKey, products);
  
  return products;
}
Javascript
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/api/data',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=60, stale-while-revalidate=120',
          },
        ],
      },
    ];
  },
};
Jsx
'use client';

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(res => res.json());

export default function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher, {
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
    refreshInterval: 60000, // Refresh every minute
    dedupingInterval: 2000, // Dedupe requests within 2 seconds
  });
  
  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return <div>Hello {data.name}!</div>;
}
Jsx
'use client';

import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // Data fresh for 1 minute
      cacheTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
      retry: 3,
      refetchOnWindowFocus: false,
    },
  },
});

function ProductList() {
  const { data, isLoading } = useQuery({
    queryKey: ['products'],
    queryFn: () => fetch('/api/products').then(res => res.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
  
  if (isLoading) return <Skeleton />;
  
  return <ProductGrid products={data} />;
}

export default function App({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}
Javascript
// app/layout.jsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
  preload: true,
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
  weight: ['400', '700'],
});

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${robotoMono.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  );
}
Css
/* globals.css */
:root {
  --font-inter: 'Inter', sans-serif;
  --font-roboto-mono: 'Roboto Mono', monospace;
}

body {
  font-family: var(--font-inter);
}

code {
  font-family: var(--font-roboto-mono);
}
Javascript
// app/layout.jsx
import localFont from 'next/font/local';

const customFont = localFont({
  src: [
    {
      path: '../public/fonts/custom-regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../public/fonts/custom-bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-custom',
  display: 'swap',
  preload: true,
});
Javascript
// lib/analytics.js
export function initAnalytics() {
  // Vercel Analytics
  if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'production') {
    import('@vercel/analytics').then(({ inject }) => inject());
  }
  
  // DataDog RUM
  if (typeof window !== 'undefined') {
    import('@datadog/browser-rum').then(({ datadogRum }) => {
      datadogRum.init({
        applicationId: process.env.NEXT_PUBLIC_DD_APP_ID,
        clientToken: process.env.NEXT_PUBLIC_DD_CLIENT_TOKEN,
        site: 'datadoghq.com',
        service: 'nextjs-app',
        env: process.env.NEXT_PUBLIC_VERCEL_ENV,
        sampleRate: 100,
        trackInteractions: true,
        defaultPrivacyLevel: 'mask-user-input'
      });
    });
  }
}
Javascript
// lib/performance.js
export class PerformanceMonitor {
  static measureNavigation() {
    if (typeof window === 'undefined') return;
    
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0];
      
      const metrics = {
        dns: navigation.domainLookupEnd - navigation.domainLookupStart,
        tcp: navigation.connectEnd - navigation.connectStart,
        ttfb: navigation.responseStart - navigation.requestStart,
        download: navigation.responseEnd - navigation.responseStart,
        domParse: navigation.domContentLoadedEventEnd - navigation.responseEnd,
        total: navigation.loadEventEnd - navigation.fetchStart
      };
      
      // Send to analytics
      fetch('/api/performance', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(metrics)
      });
    });
  }
  
  static measureResource(resourceUrl) {
    const resources = performance.getEntriesByType('resource');
    const resource = resources.find(r => r.name.includes(resourceUrl));
    
    if (resource) {
      console.log(`${resourceUrl} took ${resource.duration}ms`);
    }
  }
}
Javascript
// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      // Fail build if bundle exceeds limits
      config.performance = {
        maxAssetSize: 244000,  // 244 KB
        maxEntrypointSize: 244000,
        hints: 'error'
      };
    }
    return config;
  }
};
Json
// vercel.json
{
  "functions": {
    "pages/api/heavy-computation.js": {
      "maxDuration": 10,
      "memory": 1024
    }
  },
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "https://api.example.com/:path*"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        },
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-XSS-Protection",
          "value": "1; mode=block"
        }
      ]
    }
  ],
  "regions": ["iad1", "sfo1"]
}
Javascript
// next.config.js
const config = {
  reactStrictMode: true,
  poweredByHeader: false,
  compress: true,
};

if (process.env.NODE_ENV === 'production') {
  config.compiler = {
    removeConsole: {
      exclude: ['error', 'warn'],
    },
  };
  
  config.experimental = {
    optimizeCss: true,
    modern: true,
  };
}

module.exports = config;
  • Lighthouse score > 90 for all key pages
  • All images optimized with next/image
  • Fonts loaded with next/font
  • Code splitting implemented for heavy components
  • Bundle size < 250KB (initial load)
  • LCP < 2.5s on 3G connection
  • CLS < 0.1
  • FID < 100ms
  • API routes cached appropriately
  • Database queries optimized (no N+1)
  • Performance monitoring configured
Javascript
// Auto-generated performance regression alerts
// .github/workflows/performance.yml
name: Performance Regression Check
on: [pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run build
      - name: Check bundle size
        run: |
          npm install -g bundlesize
          bundlesize
      - name: Lighthouse CI
        run: npm run lighthouse:ci
Performance optimization in Next.js is an ongoing process. Start by measuring your current performance using tools like Lighthouse and Web Vitals. Focus on the biggest bottlenecks first, and continuously monitor the impact of your optimizations. Key takeaways:
  1. Measure first: Use RUM and synthetic monitoring
  2. Optimize assets: Images, fonts, and JavaScript bundles
  3. Choose the right rendering: SSG, ISR, SSR, or App Router based on use case
  4. Cache aggressively: At CDN, server, and client levels
  5. Monitor continuously: Set up alerts for performance regressions
  6. Set budgets: Enforce performance budgets in CI/CD
Remember: premature optimization is the root of all evil. Profile first, optimize second, and always test in production-like environments. With these techniques, you can build Next.js applications that load instantly, respond immediately, and delight users—giving you a competitive edge in today's performance-critical landscape.
On this page
Hey! Got questions?Ask me anything 👋