Published June 16, 2025

How to Reduce JavaScript Bundle Size in Next.js

In Next.js, this matters a lot because smaller bundles mean faster page loads, less JavaScript parsing, better Core Web Vitals, and a smoother experience on slower devices.

5 min read

You add a chart library, a date utility, an icon pack, a rich text editor, maybe a few UI components — and suddenly your users are downloading far more JavaScript than they need.

In Next.js, this matters a lot because smaller bundles mean faster page loads, less JavaScript parsing, better Core Web Vitals, and a smoother experience on slower devices. Next.js already helps with code splitting and tree-shaking, but you still need to be careful about what you ship to the browser.  

1. Start by analyzing your bundle

Do not optimize blindly. First, find out what is actually inside your JavaScript bundle.

For Webpack-based Next.js builds, install the official bundle analyzer:

npm install @next/bundle-analyzer

Then update next.config.js:

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

const nextConfig = {}

module.exports = withBundleAnalyzer(nextConfig)

Run:

ANALYZE=true npm run build

This opens a visual report showing which packages and modules are taking the most space. Next.js recommends using this regularly during development and before deployment so large bundles do not surprise you later.   

2. Use Server Components wherever possible

In the App Router, components are Server Components by default. That is a huge advantage.

Only add this when you really need browser-side interactivity:

'use client'

Every Client Component and everything it imports can become part of the browser bundle. So avoid putting heavy logic, data formatting, markdown parsing, syntax highlighting, or business rules inside Client Components.

Bad example:

'use client'

import Highlight from 'prism-react-renderer'

export default function BlogCodeBlock() {
  return <Highlight code={code} language="tsx" />
}

Better: render the static result on the server and send plain HTML to the browser.

import { codeToHtml } from 'shiki'

export default async function BlogCodeBlock() {
  const html = await codeToHtml(code, {
    lang: 'tsx',
    theme: 'github-dark',
  })

  return <code dangerouslySetInnerHTML={{ __html: html }} />
}

Next.js gives a similar example: moving syntax highlighting to a Server Component keeps the highlighting library out of the client bundle.  

3. Dynamically import heavy components

Not every component needs to load immediately. Charts, maps, modals, rich text editors, video players, analytics widgets, and admin-only tools are good candidates for lazy loading.

import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <p>Loading chart...</p>,
})

This keeps the chart code out of the initial bundle and loads it only when needed.

You can also dynamically import libraries inside event handlers:

async function handleSearch(value: string) {
  const Fuse = (await import('fuse.js')).default

  const fuse = new Fuse(items, {
    keys: ['title', 'description'],
  })

  return fuse.search(value)
}

Next.js documents this pattern as a way to reduce JavaScript loaded during the initial page load, especially for third-party libraries.  

4. Be careful with package imports

A common bundle-size mistake is importing an entire library when you only need one function.

Avoid:

import _ from 'lodash'

const result = _.debounce(fn, 300)

Prefer:

import debounce from 'lodash/debounce'

const result = debounce(fn, 300)

Or use packages designed for tree-shaking:

import { debounce } from 'lodash-es'

Next.js also has optimizePackageImports, which can help with packages that export hundreds or thousands of modules. It allows you to write convenient named imports while only loading the modules you actually use.  

module.exports = {
  experimental: {
    optimizePackageImports: ['package-name'],
  },
}

Note: this feature is currently experimental, so be careful before relying on it heavily in production.  

Next.js already optimizes many popular libraries by default, including lucide-react, date-fns, lodash-es, @mui/material, recharts, react-icons/*, and others.  

5. Replace large dependencies

Sometimes the best optimization is removing the package entirely.

Examples:

Instead of

Consider

moment

date-fns, dayjs, native Intl

Large chart library

Lazy-load it or use a smaller chart package

Full icon packs

Import only specific icons

Heavy rich text editor

Load only on edit pages

Client-side markdown parser

Render markdown on the server

The goal is not to avoid libraries. The goal is to avoid shipping large libraries to users who do not need them.

6. Split admin, dashboard, and marketing code

A common mistake is sharing too much code between public pages and private dashboard pages.

For example, your homepage probably does not need:

import '@/components/admin/DataTable'
import '@/components/billing/StripeManager'
import '@/components/charts/RevenueChart'

Keep public marketing pages light. Put dashboard-only components inside dashboard routes. Next.js already code-splits by route, but shared layouts and shared Client Components can still pull unnecessary JavaScript into places where it does not belong.

7. Watch your layout files

In the App Router, layout.tsx wraps many pages. If you make a root layout a Client Component, you may accidentally push a lot of your app into the client bundle.

Avoid this:

'use client'

export default function RootLayout({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>
}

Better:

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ClientProviders>{children}</ClientProviders>
      </body>
    </html>
  )
}

Then isolate only the provider logic:

'use client'

export function ClientProviders({ children }) {
  return <ThemeProvider>{children}</ThemeProvider>
}

This keeps the root layout server-rendered while still allowing client-side providers where needed.

8. Remove unused dependencies

Old packages often stay in a project long after they stop being used.

Run:

npm prune
npm dedupe

You can also inspect dependencies with:

npm ls

Or use tools like:

npx depcheck

Before removing anything, verify it is not used in build scripts, config files, dynamic imports, or generated code.

9. Optimize third-party scripts

Third-party scripts can hurt performance as much as your own JavaScript.

Use Next.js Script:

import Script from 'next/script'

<Script
  src="https://example.com/widget.js"
  strategy="lazyOnload"
/>

Use the right loading strategy:

<Script strategy="afterInteractive" />
<Script strategy="lazyOnload" />

Do not load analytics, chat widgets, heatmaps, or ads on pages where they are not needed.