EmpireUI
Get Pro
← Blog7 min read#nextjs#bundle-analyzer#performance

Next.js Bundle Analyzer: Finding and Fixing Size Issues

Stop shipping bloated JavaScript. Learn how to use Next.js Bundle Analyzer to find what's wrecking your page weight and actually fix it.

Terminal window showing webpack bundle analysis output with colorful treemap visualization

Why Your Next.js App Is Slower Than It Should Be

Honestly, most Next.js apps ship way more JavaScript than they need to. You add a date picker, a charting library, maybe a rich text editor — and six months later your first-load JS is sitting at 900kB and nobody's sure why.

The thing is, Next.js does a lot of automatic optimization out of the box. Code splitting per route, React Server Components, image optimization — it's all there. But it can't save you from yourself when you import moment.js to format a single date string, or when you pull in all of lodash for one debounce call.

That's exactly where @next/bundle-analyzer comes in. It gives you a visual treemap of every module in your client bundles, broken down by size. Once you see it, you can't unsee it. That 400kB chunk labeled node_modules/some-animation-library hits differently when it's rendered as a giant purple rectangle taking up half your screen.

Installing and Configuring @next/bundle-analyzer

Setup takes about two minutes. Install the package, wrap your Next.js config, and you're done.

npm install --save-dev @next/bundle-analyzer

Then in your next.config.js (or next.config.ts if you're on Next.js 15+): ``js // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }); /** @type {import('next').NextConfig} */ const nextConfig = { // your existing config }; module.exports = withBundleAnalyzer(nextConfig); ` Then add a script to your package.json: `json { "scripts": { "analyze": "ANALYZE=true next build" } } ` Run npm run analyze` and it'll open two browser tabs — one for the client bundle, one for the server bundle. The client one is almost always the interesting one.

Reading the Treemap: What You're Actually Looking At

The treemap is organized by bundle chunk. Each rectangle represents a module or group of modules. Bigger rectangle = more bytes. The color coding groups things by chunk, not by size category, so don't read too much into the colors.

What you're hunting for are large rectangles inside node_modules. Your own application code is usually tiny compared to dependencies. If you see react-pdf, recharts, @aws-sdk, or similar sitting there at 200kB+ each, those are your targets.

There's a gzip toggle in the top-left corner. Always check both views. A library might be 500kB parsed but compress down to 90kB gzipped — still significant, but the actual network transfer cost is the gzipped number. The parsed size matters for parse and execution time on lower-end devices.

The Most Common Bundle Killers (and How to Fix Them)

Some patterns show up over and over again. Moment.js is the classic offender — it pulls in all locale data by default, adding roughly 232kB parsed. Swap it for date-fns (import individual functions, ~2-5kB per function) or the native Intl.DateTimeFormat API if you don't need heavy formatting.

Lodash is another one. If you're doing import _ from 'lodash' you're loading the entire 70kB library. Use named imports with lodash-es instead: import { debounce } from 'lodash-es'. Tree-shaking will do its job and you'll end up with maybe 3kB.

Icon libraries deserve special attention. react-icons is great but if you import from the top-level barrel: import { FiCheck, FiX } from 'react-icons/fi' — that's actually fine, each sub-path like /fi is its own module. What kills you is import * as Icons from 'react-icons'. Same story with @mui/icons-material. Always import from the specific icon path.

Heavy charting libraries like recharts or chart.js should almost always be lazy-loaded. If the chart only appears after a user interaction or on a specific route, there's no reason to put it in the initial bundle. Use next/dynamic with ssr: false and you'll shave 200-300kB off your first-load JS.

Dynamic Imports and Code Splitting in Next.js

Next.js splits code at the route level automatically, but that's not always enough. Components that are conditionally rendered, modals, heavy UI widgets — those are all candidates for dynamic import.

import dynamic from 'next/dynamic';

// Heavy chart component — only loads when rendered
const SalesChart = dynamic(() => import('@/components/SalesChart'), {
  ssr: false,
  loading: () => <div className="h-64 bg-gray-100 animate-pulse rounded-lg" />,
});

// Modal that only opens on user action
const SettingsModal = dynamic(() => import('@/components/SettingsModal'), {
  ssr: false,
});

export default function Dashboard() {
  const [showSettings, setShowSettings] = useState(false);

  return (
    <div>
      <SalesChart />
      {showSettings && <SettingsModal onClose={() => setShowSettings(false)} />}
    </div>
  );
}

The loading prop is worth spending time on. A skeleton that matches your component's dimensions prevents layout shift while the chunk loads. If you're already using component libraries or working with visual styles from tools like glassmorphism generator or gradient generator, you can match the skeleton to your design system's color palette easily.

Analyzing Server vs Client Bundles

The analyzer opens two tabs: client.html and server.html. Most performance advice focuses on the client bundle, and for good reason — that's what gets shipped to the browser. But the server bundle matters too for cold start time, especially if you're on serverless functions.

Server bundle issues are usually large utility libraries imported in API routes or server components. Things like aws-sdk (use the modular v3 packages: @aws-sdk/client-s3 instead of the full SDK), heavy ORMs with all plugins loaded, or PDF generation libraries that get imported in a route that only runs occasionally.

Worth noting: React Server Components (available properly since Next.js 13.4+) let you keep large data-fetching dependencies entirely server-side. They never touch the client bundle at all. If you're on an older pattern — API routes plus client-side fetching — migrating data-heavy components to RSC can dramatically reduce what ends up in the client treemap. Is it always worth the refactor? Depends on your app's architecture, but it's worth measuring.

Setting a Bundle Size Budget

Finding problems once is fine. Preventing them from coming back is the actual goal. You can set a size budget in Next.js that will warn (or error) during build when bundles exceed a threshold.

// next.config.js
const nextConfig = {
  experimental: {
    // Warn when a page's JS exceeds this size (in kB)
  },
  // Built-in: Next.js warns when First Load JS > 130kB by default
  // You can also use bundlePagesRouterDependencies for Pages Router
};
```

For more control, `bundlesize` or `size-limit` as a CI step is the move:

```json
// package.json — using size-limit
{
  "size-limit": [
    {
      "path": ".next/static/chunks/pages/index-*.js",
      "limit": "120 kB"
    }
  ],
  "scripts": {
    "size": "size-limit"
  }
}

Pair this with your CI pipeline and you've got a hard gate preventing bundle regressions. When someone adds a new dependency that pushes the homepage bundle over your limit, the build fails and the PR can't merge without a conscious decision to raise the budget or find an alternative. That's the kind of guardrail that actually sticks.

If your UI relies on visual effects like shadows or complex CSS — check out our guides on Tailwind shadows generator and box shadow CSS guide for CSS-only approaches that add zero kB to your JavaScript bundle.

Practical Checklist Before Your Next Deploy

Run npm run analyze and open the client treemap. Are any individual modules above 100kB? Write them down. Check if there are duplicate versions of the same library — sometimes you end up with react-dom 18.2.0 AND 18.3.0 in the same bundle because two packages have conflicting peer deps.

Use npm ls <package-name> to trace where a dependency comes from. Use webpack-bundle-analyzer's search box (top right) to find specific modules. Run next build and look at the route-by-route First Load JS table in the terminal — Next.js prints it automatically, and anything over ~130kB on a route that should be lightweight is a red flag.

Finally, don't optimize everything at once. Pick the two or three biggest wins from your treemap, fix those, measure the result, then move on. Chasing every kB at once leads to over-engineered lazy loading of things that don't matter. The 400kB date library you removed is worth ten times more than micro-optimizing a 4kB utility you wrote yourself.

FAQ

Does @next/bundle-analyzer work with Next.js App Router and Turbopack?

Yes, it works with App Router. Turbopack (the new default dev server in Next.js 15) doesn't yet support bundle analysis directly — the analyzer only runs on webpack builds. For analysis, run next build without --turbo. Production builds still use webpack by default in Next.js 15.x as of late 2026.

What's a reasonable First Load JS target per route?

Next.js itself uses ~130kB as an informal warning threshold, but the real target depends on your users. For a developer tool or SaaS dashboard, 200kB total (shared chunks + route chunk) is acceptable. For a content site or landing page, aim for under 100kB. The shared chunks — framework code, common components — are cached after the first visit, so per-route chunks are more critical for subsequent page navigations.

How do I find which import is causing a large module to appear?

Use the search box in the treemap to find the module, then hover over it to see its full path. To trace where it's imported from in your code, run node --experimental-vm-modules node_modules/.bin/next build with NEXT_VERBOSE=true, or use webpack-stats-analyzer. Alternatively, add import-cost to your VS Code extensions — it shows the bundle cost of each import inline as you write code.

Can I use bundle analysis in a monorepo with multiple Next.js apps?

Yes. You run the analysis from each app's directory individually. If you're using Turborepo, add an analyze task to turbo.json and you can run it across all apps at once. Each app generates its own client.html and server.html treemaps. Shared packages from your monorepo show up in the treemap under their package name, making it easy to see which shared components are contributing to bundle size.

My treemap shows the same library appearing in multiple chunks. Is that a problem?

It can be. Next.js has a splitChunks configuration that tries to deduplicate shared modules into common chunks, but it doesn't always catch everything — especially when packages have different versions or import paths. Check if you can consolidate to a single version using resolutions in package.json (Yarn) or overrides (npm 8.3+). If it's the same version appearing in multiple chunks, you may need to adjust the splitChunks config in your webpack configuration.

Does switching to CSS-only animations reduce bundle size?

Yes, significantly. JavaScript animation libraries like Framer Motion (around 50kB gzipped) or GSAP add real weight. CSS animations and transitions are zero-kB additions to your JavaScript bundle — you pay only the CSS file size, which is typically a few kB and cached aggressively. For UI polish like hover effects, loading states, and simple transitions, CSS is almost always the right call.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Analyzing Next.js Bundle: @next/bundle-analyzer in CIBundle Size Analysis in React: webpack-bundle-analyzer, rollup-visualizerNext.js Performance Checklist 2026: Every Optimization, RankedNext.js Middleware: Auth, Redirects, A/B Tests at the Edge