May 7, 2026
Next.js Bundle Analyzer for App Router Performance Reviews
A practical guide to using Next.js bundle analyzer in App Router projects, reading client chunks, finding heavy dependencies, and reducing bundle size safely.
8 min read
Next.js Bundle Analyzer for App Router Performance Reviews
If you are searching for Next.js bundle analyzer, you are probably trying to answer a concrete question: why did this route become heavier than expected? The App Router gives you Server Components, route-level splitting, layouts, streaming, and dynamic imports, but those tools do not automatically explain where client JavaScript is coming from. A page can look simple while a shared provider, client utility barrel, charting package, editor, icon import, or vendor SDK quietly enters the first load path.
This guide shows how to analyze Next.js bundle App Router output without turning the review into a guessing exercise. You will learn where bundle analyzer fits, what to look for in client chunks, how to connect findings to route architecture, and how to reduce Next.js bundle size without breaking useful interactivity. It builds directly on Next.js Performance Budget for App Router Teams, Next.js Dynamic Imports and Code Splitting for Faster App Router Apps, and Next.js Client Components Best Practices for App Router Apps. For broader site visibility, pair this work with Next.js SEO Checklist for App Router Projects, because fast pages are easier to crawl, render, and use.
Start with the question, not the chart
Bundle analyzer creates a visual map of JavaScript output. That is useful, but only after you know what you are investigating. Otherwise the chart becomes a colorful distraction.
Before running tools, write down the route and concern:
- did first-load JavaScript increase?
- did a route start hydrating slowly?
- did a new library appear in a shared chunk?
- did a Server Component accidentally become a Client Component?
- did a dynamic import fail to move code out of the initial path?
- did an optimization help one route but hurt another?
That framing matters because Next.js bundle analyzer does not tell you whether a dependency is valuable. It shows what shipped. Engineering judgment still decides whether that cost belongs on the first route, a later interaction, a narrower layout, or the server.
Install bundle analyzer for a focused review
In most projects, use the official analyzer plugin wrapper:
npm install --save-dev @next/bundle-analyzer
Then wire it into next.config.mjs behind an environment variable so normal builds stay quiet:
// next.config.mjs
import createBundleAnalyzer from "@next/bundle-analyzer";
const withBundleAnalyzer = createBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default withBundleAnalyzer(nextConfig);
Run a production build with analysis enabled:
ANALYZE=true npm run build
The generated reports show server, edge, and client output. For App Router performance reviews, start with the client report. That is the JavaScript users download and execute in the browser.
Read App Router chunks by ownership
When you analyze Next.js bundle App Router output, group what you see by ownership rather than file shape. A large rectangle is not automatically wrong. The issue is whether it belongs in the first route experience.
Useful ownership buckets:
| Bucket | Common source | Review question |
|---|---|---|
| Shared framework | React, Next.js runtime | Is this expected baseline? |
| Shared app shell | root layout, providers, nav | Does every route need this? |
| Route feature | dashboard, editor, chart | Is it scoped to the route? |
| Optional interaction | modal, command palette, export | Can it load after intent? |
| Vendor script or SDK | analytics, maps, chat | Can it move to next/script or a server route? |
This ownership view is more practical than debating one package in isolation. A charting library can be appropriate on an analytics route and inappropriate in the root layout. A markdown editor can be fine inside an admin composer and wasteful on a public blog post.
Watch shared layouts and providers first
The fastest way to spread client JavaScript across an App Router app is to put broad Client Components near the top of the tree. A root provider that imports feature code makes that feature part of the shared client path.
Weak pattern:
"use client";
import { ThemeProvider } from "next-themes";
import { CommandPalette } from "@/components/command-palette";
import { ReportExportDialog } from "@/components/reports/ReportExportDialog";
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
{children}
<CommandPalette />
<ReportExportDialog />
</ThemeProvider>
);
}
Even if the export dialog is hidden, its imports may still enter the bundle. A better approach keeps global providers boring and moves feature UI closer to the route or interaction that needs it:
"use client";
import { ThemeProvider } from "next-themes";
export function AppProviders({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
Then load the report export UI from the report route, ideally after user intent. This connects directly to the server/client boundary guidance in Next.js Client Components Best Practices for App Router Apps.
Check whether dynamic imports actually helped
Dynamic imports are useful only when they move non-critical code out of the initial path. After adding next/dynamic, run the analyzer again and confirm the expensive dependency is no longer in the route's first-load client chunk.
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("./RevenueChart"), {
loading: () => <div className="h-72 rounded-md border" />,
});
export function DashboardOverview() {
return (
<section>
<h2>Revenue trend</h2>
<RevenueChart />
</section>
);
}
If the charting library still appears in the initial bundle, look for another import path. It may be re-exported from a shared barrel, imported by a provider, or used by a component that renders above the dynamic boundary. This is why analyzer work pairs so well with Next.js Dynamic Imports and Code Splitting for Faster App Router Apps: the goal is not to add dynamic imports everywhere, but to place the boundary where it changes the shipped JavaScript.
Find expensive barrel exports
Barrel files can hide dependency cost. A convenient index.ts that re-exports every component from a folder may cause a small import to drag in unrelated client code, depending on package shape and tree-shaking behavior.
Risky pattern:
// components/dashboard/index.ts
export * from "./MetricCard";
export * from "./RevenueChart";
export * from "./MapPanel";
export * from "./RichTextNoteEditor";
Then a route imports one lightweight card:
import { MetricCard } from "@/components/dashboard";
Prefer direct imports for performance-sensitive routes:
import { MetricCard } from "@/components/dashboard/MetricCard";
This does not mean every barrel file is bad. It means bundle reviews should treat broad re-export modules as suspects when unrelated dependencies appear in the analyzer output.
Reduce Next.js bundle size with the smallest fix
Once you find a heavy dependency, choose the smallest correction that matches the cause. Avoid broad rewrites when route placement would solve the problem.
Common fixes:
- move feature components out of root layouts
- convert static UI back to Server Components
- replace a broad package import with a smaller direct import
- lazy load optional modals, charts, maps, and editors
- remove duplicate packages that solve the same job
- keep vendor secrets and privileged calls in route handlers
- scope analytics and chat widgets to the pages that need them
For example, if a date library appears because one component formats a server-rendered timestamp, move formatting to the server or use the platform API:
export function PublishedDate({ date }: { date: string }) {
const formatted = new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
year: "numeric",
}).format(new Date(date));
return <time dateTime={date}>{formatted}</time>;
}
If the component does not need browser state, keep it server-rendered. The best way to reduce Next.js bundle size is often to avoid sending code to the browser at all.
Compare before and after in the same conditions
Bundle analysis should be repeatable. Use the same branch, route, build command, and environment when comparing results. Record the reason for the change in the pull request, especially when a dependency remains large but justified.
A practical review note:
## Bundle review
- Route: /dashboard/reports
- Concern: chart package entered first-load client bundle
- Change: moved report chart behind route-level dynamic import
- Result: chart code moved to delayed chunk
- Follow-up: inspect export dialog if reports route grows again
This pairs with the release checklist from Next.js Performance Budget for App Router Teams. The analyzer explains why the budget moved. The budget explains whether that movement needs action.
Do not optimize away product value
Bundle analyzer can tempt teams into treating every kilobyte as waste. That is too simplistic. A route that helps users complete a high-value workflow can justify more JavaScript than a static article page. The key is intent.
Keep these rules practical:
- public content should ship very little client code
- marketing pages should justify each third-party script
- dashboards can carry interactive code, but shared layouts should stay lean
- editors and visual tools should split the shell from heavy panels
- admin-only features should not affect public routes
The right outcome is not the smallest possible bundle. It is the smallest bundle that preserves the experience the route is supposed to deliver.
Final takeaway
Next.js bundle analyzer is most valuable when it answers a specific route question. Use it to identify where client JavaScript comes from, then connect the finding to App Router ownership: root layout, provider, route feature, optional interaction, or vendor dependency.
To analyze Next.js bundle App Router output well, start with client chunks, inspect shared imports, verify dynamic imports actually moved code, and watch for barrel files that hide heavy dependencies. To reduce Next.js bundle size, prefer small fixes: move code to the server, narrow client boundaries, scope features to routes, and lazy load optional UI after intent.
That gives performance reviews a concrete workflow. Instead of debating whether a route "feels heavy," you can show what shipped, why it shipped, and exactly which boundary should change.