How-To GuidesPerformance Optimization

Performance Optimization

Learn how to optimize this project for better performance, faster load times, and improved user experience.

When to use

  • Your application is experiencing slow page load times
  • You want to improve Core Web Vitals scores
  • You need to reduce bundle size and improve initial load performance
  • You’re experiencing performance issues with large datasets or complex component
  • you’re getting numbers less than 90 in PageSpeed Insights or lighthouse

Checklist

  • Analyze current performance metrics using Lighthouse , PageSpeed Insights or web vitals
  • Implement code splitting and lazy loading for components
  • Optimize images and assets
  • Configure caching strategies for API routes and static content
  • Set up proper database query optimization
  • Implement server-side rendering (SSR) or static site generation (SSG) where appropriate
  • Monitor and measure performance improvements

Step-by-step

1. Analyze Performance

  1. Use Chrome DevTools Lighthouse or PageSpeed Insights (with production link ) to identify performance bottlenecks or
  2. Check Core Web Vitals: LCP, FID, CLS (see detailed explanations below)
  3. Monitor bundle size with npm run build and analyze the output
  4. Use Next.js built-in analytics or add Web Vitals tracking

2. Optimize Images

  1. Use Next.js Image component instead of standard <img> tags
  2. Enable image optimization in next.config.mjs
  3. Convert images to WebP format where possible
  4. Implement lazy loading for images below the fold
  5. Use appropriate image sizes and responsive images
  6. compress image size

3. Code Splitting and Lazy Loading

  1. Use dynamic imports for heavy components: import dynamic from 'next/dynamic'
  2. Split code at route level using Next.js automatic code splitting
  3. Lazy load third-party libraries that aren’t needed immediately
  4. Implement route-based code splitting for large pages

4. Optimize Database Queries

  1. Add proper indexes to frequently queried fields
  2. Use pagination for large datasets
  3. Implement query result caching where appropriate
  4. Avoid N+1 query problems
  5. Use database query optimization techniques (projections, aggregation pipelines)

5. Caching Strategies

  1. Implement ISR (Incremental Static Regeneration) for pages that change infrequently
  2. Use browser caching headers for static assets
  3. Implement API route caching with appropriate cache-control headers
  4. Consider using Redis or similar for server-side caching

6. Bundle Optimization

  1. Remove unused dependencies
  2. Use tree-shaking to eliminate dead code
  3. Analyze bundle size with @next/bundle-analyzer
  4. Split vendor chunks appropriately
  5. Consider using SWC compiler for faster builds

Core Web Vitals Deep Dive

Core Web Vitals are a set of metrics that Google uses to measure real-world user experience on websites. They focus on three key aspects: loading performance (LCP), interactivity (FID), and visual stability (CLS). These metrics directly impact your site’s search ranking and user satisfaction.

LCP (Largest Contentful Paint)

Definition: LCP measures when the largest content element visible in the viewport becomes fully rendered. This is typically the main hero image, a large text block, or a video. It’s a key indicator of perceived loading speed.

Thresholds:

  • Good: Less than 2.5 seconds
  • Needs Improvement: Between 2.5 and 4 seconds
  • Poor: Greater than 4 seconds

What Elements Typically Contribute to LCP:

  • Largest image or video in the viewport
  • Large text blocks (headings, paragraphs)
  • Background images with text overlays
  • Hero banners or carousels

Common Causes of Poor LCP:

  • Slow server response times
  • Render-blocking JavaScript or CSS
  • Large, unoptimized images
  • Slow resource load times (fonts, images, stylesheets)
  • Client-side rendered content
  • Large or unoptimized web fonts

Optimization Strategies:

  1. Optimize Server Response Times

    • Use a CDN to serve content closer to users
    • Implement server-side caching
    • Optimize database queries
    • Use edge computing where possible
  2. Preload Critical Resources

    // pages/_document.js
    import { Html, Head } from 'next/document';
     
    export default function Document() {
      return (
        <Html>
          <Head>
            {/* Preload critical resources */}
            <link
              rel="preload"
              href="/fonts/main-font.woff2"
              as="font"
              type="font/woff2"
              crossOrigin="anonymous"
            />
            <link
              rel="preload"
              href="/hero-image.jpg"
              as="image"
            />
          </Head>
        </Html>
      );
    }
  3. Reduce Render-Blocking Resources

    // next.config.mjs
    export default {
      experimental: {
        optimizeCss: true,
      },
    };
    // Use async or defer for non-critical scripts
    <script src="analytics.js" defer></script>
  4. Optimize Images

    // Use Next.js Image component with priority for LCP element
    import Image from 'next/image';
     
    export default function Hero() {
      return (
        <Image
          src="/hero-image.jpg"
          alt="Hero"
          width={1920}
          height={1080}
          priority // Critical for LCP
          quality={85}
          placeholder="blur"
        />
      );
    }
  5. Optimize Web Fonts

    /* styles/globals.css */
    @font-face {
      font-family: 'CustomFont';
      src: url('/fonts/custom-font.woff2') format('woff2');
      font-display: swap; /* Prevents invisible text during font load */
      font-weight: 400;
    }
  6. Use CDN for Static Assets

    // next.config.mjs
    export default {
      images: {
        domains: ['cdn.yoursite.com'],
      },
      assetPrefix: process.env.NODE_ENV === 'production' 
        ? 'https://cdn.yoursite.com' 
        : '',
    };

FID (First Input Delay)

Definition: FID measures the time from when a user first interacts with your page (click, tap, key press) to when the browser is able to respond to that interaction. It quantifies the responsiveness of your page.

Note: FID is being replaced by INP (Interaction to Next Paint) in 2024, which measures all interactions, not just the first one. However, FID remains important for legacy tracking.

Thresholds:

  • Good: Less than 100 milliseconds
  • Needs Improvement: Between 100 and 300 milliseconds
  • Poor: Greater than 300 milliseconds

Common Causes of Poor FID:

  • Long JavaScript execution tasks blocking the main thread
  • Heavy JavaScript bundles parsing and executing
  • Third-party scripts (analytics, ads, widgets)
  • Large, unoptimized components rendering
  • Synchronous operations blocking user input

Optimization Strategies:

  1. Reduce JavaScript Execution Time

    // Break up long tasks using setTimeout
    function processLargeDataset(data) {
      // Instead of processing all at once:
      // processAll(data); // ❌ Blocks main thread
      
      // Process in chunks:
      let index = 0;
      function processChunk() {
        const chunk = data.slice(index, index + 100);
        processChunk(chunk);
        index += 100;
        
        if (index < data.length) {
          setTimeout(processChunk, 0); // Yield to browser
        }
      }
      processChunk();
    }
  2. Optimize Third-Party Scripts

    // Load non-critical scripts asynchronously
    useEffect(() => {
      const script = document.createElement('script');
      script.src = 'https://third-party-widget.com/script.js';
      script.async = true;
      script.defer = true;
      document.body.appendChild(script);
    }, []);
  3. Code Splitting and Lazy Loading

    // Load heavy components only when needed
    import dynamic from 'next/dynamic';
     
    const HeavyChart = dynamic(
      () => import('../components/HeavyChart'),
      { 
        loading: () => <div>Loading chart...</div>,
        ssr: false // Don't block SSR
      }
    );
  4. Defer Non-Critical JavaScript

    // Use requestIdleCallback for non-urgent tasks
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        // Non-critical initialization
        initializeAnalytics();
      });
    } else {
      setTimeout(() => initializeAnalytics(), 2000);
    }

CLS (Cumulative Layout Shift)

Definition: CLS measures visual stability by quantifying how much visible content shifts during page load. It calculates the sum of all layout shift scores for unexpected shifts that occur during the lifespan of the page.

Thresholds:

  • Good: Less than 0.1
  • Needs Improvement: Between 0.1 and 0.25
  • Poor: Greater than 0.25

Common Causes of Layout Shifts:

  • Images without width and height attributes
  • Fonts loading causing FOIT (Flash of Invisible Text) or FOUT (Flash of Unstyled Text)
  • Dynamically injected content without reserved space
  • Ads, embeds, or iframes without dimensions
  • Dynamically inserted content above existing content
  • Web fonts causing text reflow

Optimization Strategies:

  1. Set Size Attributes on Images and Videos

    // Always specify dimensions
    import Image from 'next/image';
     
    <Image
      src="/image.jpg"
      width={800}
      height={600}
      alt="Description"
    />
     
    // Or use aspect ratio box
    <div style={{ aspectRatio: '16/9', position: 'relative' }}>
      <Image
        src="/video-thumbnail.jpg"
        fill
        alt="Video thumbnail"
      />
    </div>
  2. Reserve Space for Ads and Embeds

    // Reserve space for ad containers
    <div 
      style={{
        minHeight: '250px', // Reserve space
        width: '100%',
        backgroundColor: '#f5f5f5' // Placeholder color
      }}
    >
      {/* Ad will load here */}
    </div>
  3. Optimize Font Loading

    /* Use font-display: swap to prevent invisible text */
    @font-face {
      font-family: 'CustomFont';
      src: url('/fonts/custom-font.woff2') format('woff2');
      font-display: swap; /* Show fallback immediately */
    }
    // Preload critical fonts
    // pages/_document.js
    <link
      rel="preload"
      href="/fonts/custom-font.woff2"
      as="font"
      type="font/woff2"
      crossOrigin="anonymous"
    />
  4. Avoid Inserting Content Above Existing Content

    // ❌ Bad: Inserts above existing content
    function BadComponent() {
      const [showBanner, setShowBanner] = useState(false);
      
      return (
        <div>
          {showBanner && <Banner />} {/* Causes shift */}
          <MainContent />
        </div>
      );
    }
     
    // ✅ Good: Reserve space or use absolute positioning
    function GoodComponent() {
      return (
        <div>
          <div style={{ minHeight: '60px' }}>
            {/* Reserved space for banner */}
          </div>
          <MainContent />
        </div>
      );
    }
  5. Set Dimensions for Embeds

    // YouTube embed with aspect ratio
    <div style={{ position: 'relative', paddingBottom: '56.25%' }}>
      <iframe
        style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }}
        src="https://www.youtube.com/embed/VIDEO_ID"
        frameBorder="0"
      />
    </div>
  6. Use CSS to Reserve Space

    /* Reserve space for dynamic content */
    .dynamic-content {
      min-height: 400px; /* Reserve space */
    }
     
    /* Use aspect-ratio for responsive containers */
    .image-container {
      aspect-ratio: 16 / 9;
    }

Measuring Core Web Vitals

Chrome DevTools:

  1. Open Chrome DevTools (F12)
  2. Go to the “Performance” tab
  3. Click Record and interact with your page
  4. Stop recording and check the “Web Vitals” section in the timeline
  5. Look for LCP, FID, and CLS markers

PageSpeed Insights:

  1. Visit PageSpeed Insights
  2. Enter your production URL
  3. Click “Analyze”
  4. Review the Core Web Vitals section in the results
  5. Check both lab and field data

Real User Monitoring (RUM) in Next.js:

// pages/_app.js
import { useEffect } from 'react';
import { getCLS, getFID, getLCP } from 'web-vitals';
 
export function reportWebVitals(metric) {
  // Send to your analytics service
  console.log(metric);
  
  // Example: Send to Google Analytics
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('event', metric.name, {
      value: Math.round(metric.value),
      event_label: metric.id,
      non_interaction: true,
    });
  }
  
  // Or send to your own API
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify(metric),
    headers: { 'Content-Type': 'application/json' },
  });
}
 
export default function App({ Component, pageProps }) {
  useEffect(() => {
    // Measure Core Web Vitals
    getCLS(reportWebVitals);
    getFID(reportWebVitals);
    getLCP(reportWebVitals);
  }, []);
 
  return <Component {...pageProps} />;
}

Using Next.js Built-in Web Vitals:

// pages/_app.js
export function reportWebVitals(metric) {
  // metric will include: name, value, id, delta
  // name can be: CLS, FID, LCP, FCP, TTFB, etc.
  
  switch (metric.name) {
    case 'LCP':
      console.log('LCP:', metric.value, 'ms');
      break;
    case 'FID':
      console.log('FID:', metric.value, 'ms');
      break;
    case 'CLS':
      console.log('CLS:', metric.value);
      break;
  }
}

Gotchas

  • Don’t over-optimize prematurely; measure first, then optimize based on data
  • Lazy loading can improve initial load but may cause layout shifts if not handled properly
  • Image optimization requires server resources; ensure your hosting supports it
  • Caching can lead to stale data if not configured properly
  • Code splitting increases the number of requests; balance with HTTP/2 multiplexing
  • Database query optimization requires understanding your data access patterns

Optimization Code Examples

Dynamic Import Example

import dynamic from 'next/dynamic';
 
// Lazy load heavy component
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
  loading: () => <p>Loading...</p>,
  ssr: false, // Disable SSR if component doesn't need it
});
 
export default function Page() {
  return (
    <div>
      <HeavyComponent />
    </div>
  );
}

Image Optimization Example

import Image from 'next/image';
 
export default function OptimizedImage() {
  return (
    <Image
      src="/hero-image.jpg"
      alt="Hero image"
      width={800}
      height={600}
      priority // Load immediately for above-the-fold images
      placeholder="blur" // Optional: add blur placeholder
    />
  );
}

API Route Caching Example

// pages/api/data.js
export default async function handler(req, res) {
  // Set cache headers
  res.setHeader(
    'Cache-Control',
    'public, s-maxage=60, stale-while-revalidate=300'
  );
 
  const data = await fetchData();
  
  return res.status(200).json(data);
}

ISR Example

// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const post = await getPost(params.slug);
 
  return {
    props: { post },
    revalidate: 3600, // Revalidate every hour
  };
}
 
export async function getStaticPaths() {
  const posts = await getAllPosts();
 
  return {
    paths: posts.map((post) => ({ params: { slug: post.slug } })),
    fallback: 'blocking', // or 'true' for better performance
  };
}

Performance Monitoring

Web Vitals Tracking

// pages/_app.js
export function reportWebVitals(metric) {
  // Send to analytics service
  console.log(metric);
  
  // Example: Send to Google Analytics
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('event', metric.name, {
      value: Math.round(metric.value),
      event_label: metric.id,
      non_interaction: true,
    });
  }
}

Resources