Next.js’s bundle analysis is a powerful tool, but most developers only scratch the surface, using it to spot the biggest offenders without understanding how deeply nested dependencies and server-side code can inflate their client-side JavaScript.
Let’s see it in action. First, you need to build your Next.js application with the analyze flag:
npm run build -- --profile
This command builds your application and generates a analyze directory within your .next folder. Inside, you’ll find a report.html file. Open this file in your browser. You’ll see a treemap visualization of your JavaScript bundles. Each box represents a module, and its size is proportional to its contribution to the total bundle.
Here’s a typical view you might encounter:

This treemap shows you the "what" – what’s taking up space. But to truly reduce your JavaScript size, you need to understand the "why" and the "how."
The core problem Next.js solves with its bundling is code splitting. Instead of sending one giant JavaScript file to the browser, Next.js breaks it down into smaller chunks that are loaded on demand. This improves initial page load performance. However, the analysis tool reveals the unintended consequences of this process: large, unoptimized bundles are still being generated, often due to common misconceptions about what’s actually being sent to the client.
The Mental Model: Client vs. Server Bundles
It’s crucial to distinguish between what Next.js bundles for the server and what it bundles for the client.
- Server Bundles: These are executed on the server during SSR (Server-Side Rendering) or API routes. They include your page components, data fetching logic (
getServerSideProps,getStaticProps), and any server-side dependencies. These do not directly impact client-side download size but can affect server build times and memory usage. - Client Bundles: This is the JavaScript that runs in the user’s browser. It includes your React components, client-side logic, and any libraries that are imported and used directly in your UI. The bundle analysis tool primarily visualizes these client-side chunks.
The confusion often arises when a library is imported but only used in a server-only context (e.g., within getServerSideProps). Next.js is smart enough not to include these in the client bundle. However, if a module is imported anywhere that could potentially be client-side (even if it’s not in your current use case), it might end up in the client bundle.
Common Culprits and How to Tackle Them:
-
Large Libraries with Unused Features: Many libraries are modular. You might only need a small part of a large library (e.g., a specific charting component from Chart.js).
- Diagnosis: Look for large boxes in the treemap corresponding to known libraries. Drill down into them to see which specific modules are contributing the most.
- Fix: Use dynamic imports (
import()) for specific components or features. For example, instead ofimport Chart from 'chart.js';, useconst Chart = dynamic(() => import('chart.js'));. This tells Next.js to only load this module when it’s actually rendered. - Why it works: Dynamic imports create separate JavaScript chunks that are fetched only when the component using them is mounted on the client.
-
Moment.js and Lodash (Full Imports): These libraries are notorious for their size when imported in their entirety.
- Diagnosis: Spotting
momentorlodashas large top-level modules. - Fix:
- For
moment.js, consider using a lighter alternative likedate-fnsordayjs. If you must usemoment, import only the specific locales you need:import moment from 'moment/moment'; import 'moment/locale/en-gb';. - For
lodash, use the modular imports:import get from 'lodash/get';instead ofimport _ from 'lodash';. Most bundlers, including Next.js, are configured to tree-shake these modular imports effectively.
- For
- Why it works: Importing only the necessary functions or locales prevents the entire library from being included in your bundle.
- Diagnosis: Spotting
-
Unused Dependencies in
package.json: Sometimes, dependencies are left inpackage.jsonthat are no longer used in the codebase.- Diagnosis: While the bundle analysis won’t directly show unused
package.jsonentries, you’ll notice larger-than-expected bundles and might not find obvious culprits within the treemap. Runningnpm lsoryarn listcan help identify installed packages. - Fix: Remove unused dependencies from
package.jsonand runnpm installoryarn install. - Why it works: Removing unused code from your project, even if it’s just a dependency that’s no longer imported, reduces the overall footprint.
- Diagnosis: While the bundle analysis won’t directly show unused
-
Server-Only Code Leaking into Client Bundles: This is the trickiest. If a module is imported in a file that could be client-side, it might get bundled.
- Diagnosis: You see a large module in the client bundle analysis that you know should only be used server-side (e.g., a database client or a heavy Node.js utility). Check the import chains leading to this module.
- Fix: Use Next.js’s built-in
server-onlypackage. Create aserver-only.jsfile in yourlibfolder:
Then, in the module you want to restrict to the server, add// lib/server-only.js // This file is intentionally left empty. // It's used to mark modules as server-only.import 'server-only';at the top.// lib/my-server-module.js import 'server-only'; export function doServerThing() { // ... server-side logic } - Why it works: The
server-onlypackage is a special marker. During the build process, Next.js detects this import and ensures that any module importingserver-onlyis never included in client-side JavaScript bundles. If it’s accidentally imported in a client component, the build will fail.
-
Duplicate Dependencies: Different parts of your application might install the same dependency at different versions, leading to multiple copies in your
node_modulesand potentially in your bundles.- Diagnosis: The treemap might show multiple instances of the same library, or the total size seems inflated beyond what you expect from your direct dependencies. Run
npm ls <library-name>oryarn why <library-name>to see where a package is being installed from. - Fix: Use
npm dedupeoryarn dedupeto resolve duplicate dependencies. You might also need to explicitly manage peer dependencies or use tools likeresolutionsinyarnoroverridesinnpmto force a single version. - Why it works: Deduplication ensures that only one version of a library is installed and, consequently, bundled, reducing redundancy.
- Diagnosis: The treemap might show multiple instances of the same library, or the total size seems inflated beyond what you expect from your direct dependencies. Run
-
Large Assets (Fonts, Images) Included in JS Bundles: While not strictly JavaScript, sometimes large assets can be included in the bundle if not handled correctly.
- Diagnosis: You see unusually large chunks in the bundle analysis that don’t correspond to code, or your total bundle size is unexpectedly high, and code analysis doesn’t reveal the cause.
- Fix: Ensure images are optimized and served efficiently (e.g., using
next/image). For fonts, usenext/fontwhich automatically handles optimization and deduplication, or ensure fonts are loaded via CSS<link>tags and not imported as JS modules. - Why it works:
next/fontand proper asset handling prevent large binary assets from being embedded directly into JavaScript chunks, allowing them to be loaded efficiently via separate network requests.
By systematically analyzing your Next.js bundles with these points in mind, you can move beyond simply identifying large modules to understanding and fixing the root causes of bloated client-side JavaScript.
The next step after optimizing your bundles is often to consider the impact of client-side data fetching and caching strategies on perceived performance.