Back to Blog

Next.js Performance Optimization in Action: From 499KB to 154KB

March 19, 2024 (1y ago)

This article focuses on practical problem-solving in performance optimization, providing a framework and methodology. Your specific situation may require tailored solutions.

Why Do We Need Performance Optimization?

I first noticed the issue from the user experience—the website felt slower than before. So I opened the Network tab and ran Lighthouse.

I discovered that the Next.js build output _app-178bb03084901d5c.js was 499KB—way too large. Lighthouse also pointed out several other issues.

"next": "13.1.6",
"react": "18.2.0",
"react-dom": "18.2.0",

Performance under different network speeds:

Untitled.png

Core Optimization Strategies

1. Bundle Size Analysis and Optimization

2. Lighthouse and Core Web Vitals Optimization

  • Largest Contentful Paint (LCP): Measures loading performance. For good user experience, LCP should occur within 2.5 seconds of when the page first starts loading.
  • Interaction to Next Paint (INP): Measures interactivity. For good user experience, pages should have an INP of 200 milliseconds or less.
  • Cumulative Layout Shift (CLS): Measures visual stability. For good user experience, pages should maintain a CLS of 0.1 or less.

Google's performance assessment tool for optimizing website performance, accessibility, best practices, and SEO. Before optimization:

Untitled 1.png

Install [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer), a Next.js plugin that helps you manage JavaScript module sizes. It generates a visual report of each module's size and dependencies. You can use this information to remove large dependencies, split code, or load certain parts only when needed, reducing the amount of data transferred to the client.

npm i @next/bundle-analyzer
# or
yarn add @next/bundle-analyzer
# or
pnpm add @next/bundle-analyzer

Then modify next.config.js:

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})
 
/** @type {import('next').NextConfig} */
const nextConfig = {}
 
module.exports = withBundleAnalyzer(nextConfig)

Analyze dependencies:

ANALYZE=true npm run build
# or
ANALYZE=true yarn build
# or
ANALYZE=true pnpm build

After running the analysis command:

WechatIMG38825.jpg

We can see the bundle is indeed large. I found that highlight.js was loading all language packs, which wasn't necessary for me. It should use async or lightweight loading instead.

But I wasn't using highlight.js directly in my code. Using pnpm why highlight.js, I discovered it was imported by another library (react-syntax-highlighter). After finding the source of the problem, I started modifying the relevant components.

Changed react-syntax-highlighter to lightweight import:

// Lightweight import approach
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';

image.png

Untitled 2.png

3. Optimizing Cumulative Layout Shift

The "carousel" at the top is dynamically rendered based on the systemconfig API response, causing "layout shift" because it may not always exist. Setting CSS properties like display, visibility, or opacity to hide elements doesn't work.

Next.js server-side rendering solves the problem. By deciding whether the "carousel" appears on the server side, we get the result ahead of time, preventing layout shift on the client side.

export async function getServerSideProps(content: any) {
  const baseurl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`;
  const { data } = (await (await fetch(`${baseurl}/api/platform/getSystemConfig`)).json()) as {
    data: SystemConfigType;
  };
 
  return {
    props: {
      ...(await serviceSideProps(content)),
      showCarousel: data.showCarousel
    }
  };
}

4. Image Resource Optimization

First, use next/image to optimize images, reduce image size, and use the priority prop to prioritize rendering images that affect LCP.

The priority property should be used on any image detected as the Largest Contentful Paint (LCP) element. It may be appropriate to have multiple priority images, as different images may be the LCP element for different viewport sizes.

import Image from 'next/image'
 
export default function Page() {
  return (
    <Image
      src="/profile.png"
      width={500}
      height={500}
      alt="Picture of the author"
      priority
    />
  )
}

Use tinify.cn to compress image sizes and use better image formats.

5. Accessibility Optimization

Only minor improvements were made here—adding IDs to relevant buttons to improve page accessibility. Won't go into detail here.

Optimization Results Comparison

Here's the Lighthouse score after website optimization, showing significant improvement.

Untitled 3.png

Optimized dependency size reduced from 485KB to 154KB, approximately 69% improvement.

WechatIMG38829.jpg

Optimized network performance:

Snipaste_2024-03-19_22-45-19.png

Summary and Best Practices

Through this Next.js performance optimization practice, we successfully reduced the build output from 499KB to 154KB, achieving a 69% performance improvement. Main optimization strategies include:

  1. Bundle Analysis Optimization: Use @next/bundle-analyzer to identify large dependencies
  2. Core Web Vitals Optimization: Focus on LCP, INP, and CLS metrics
  3. Image Resource Optimization: Use next/image and image compression
  4. Server-Side Rendering Optimization: Reduce client-side layout shift
  5. Accessibility Optimization: Improve page accessibility

These optimization techniques apply not only to Next.js projects but also to other React projects. I hope this article helps frontend developers with similar performance optimization needs.

See you next time for Next.js internationalization and multi-language exploration!