Analisi e riduzione del bundle client
Usi il bundle analyzer per individuare le dipendenze pesanti e ridurre il JavaScript client inviato agli utenti.
Analisi e riduzione del bundle client è una lezione Next.js 15 Fullstack (App Router + Server Actions) gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Next.js 15 Fullstack (App Router + Server Actions), e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Next.js 15 Fullstack (App Router + Server Actions) include 4 lezioni in totale.
Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.
Why Client Bundle Size Matters
Every kilobyte of JavaScript your app ships to users has a cost: download time, parse time, and execution time. On a mid-range mobile device on a 4G connection, a 1 MB JS bundle can add 3–5 seconds of blocking time before the page becomes interactive.
Next.js 15 with the App Router already does a lot for you:
- Server Components never ship to the client
- Route-based code splitting is automatic
- Tree shaking removes unused exports
But third-party libraries, accidental client imports, and large utility packages can silently inflate your bundle. This lesson shows you how to find and fix those problems systematically.
Installing @next/bundle-analyzer
The official Next.js bundle analyzer wraps webpack-bundle-analyzer and integrates cleanly with next.config.ts. Install it once as a dev dependency:
After installation, you wrap your Next.js config with the analyzer factory. It reads the ANALYZE environment variable so the report only opens when you explicitly request it — your normal builds are unaffected.
The analyzer generates two interactive HTML treemaps:
- client.html — JavaScript sent to the browser
- server.html — Node.js server bundle (useful but secondary)
// Terminal
// npm install --save-dev @next/bundle-analyzer
// next.config.ts
import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: true,
});
const nextConfig: NextConfig = {
// your existing config
};
export default withBundleAnalyzer(nextConfig);Running an Analysis Build
Add a convenience script to package.json so the analysis command is memorable and consistent across your team:
When you run npm run analyze, Next.js performs a full production build and then opens two browser tabs with the treemap reports. The size of each rectangle represents the parsed (uncompressed) byte size of that module inside your bundle.
Key things to look for in the client treemap:
- Unexpectedly large rectangles from a single library
- Libraries you only use on the server appearing in the client chunk
- Multiple copies of the same library (version conflicts)
// package.json (relevant section)
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"analyze": "ANALYZE=true next build"
}
}Reading the Treemap Report
The treemap groups modules by chunk. Each production page in your App Router app typically has at least three chunk types:
- app/layout chunk — shared across all routes; anything imported in your root layout lands here
- page-specific chunks — code unique to one route
- shared chunks — modules used by two or more pages, extracted automatically by webpack
Hover over any rectangle to see the full module path and its stat size (raw), parsed size (after minification), and gzipped size (what travels over the wire). Always optimize for gzipped size — it is the real user cost.
A common finding: a date library like moment or date-fns with all locales bundled taking 200–500 KB parsed in the shared chunk.
The Accidental Client Import Problem
In the App Router, components are Server Components by default. However, the moment a Server Component imports a module that itself has a 'use client' boundary somewhere in its dependency tree — or you accidentally import a server utility into a Client Component — things go wrong.
A more common problem is the reverse: importing a heavy library inside a 'use client' component that could instead live on the server. For example, a Markdown renderer, a syntax highlighter, or a PDF parser has no business running in the browser if it only renders static content.
The fix is straightforward: move the rendering logic into a Server Component and pass the result as a prop or slot.
// BEFORE: heavy library shipped to the client
'use client';
import { marked } from 'marked'; // ~50 KB parsed
export function BlogPost({ raw }: { raw: string }) {
return <div dangerouslySetInnerHTML={{ __html: marked(raw) }} />;
}
// AFTER: render on the server, ship only HTML
// app/blog/[slug]/page.tsx (Server Component — no 'use client')
import { marked } from 'marked';
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const raw = await fetchPostMarkdown(slug);
const html = marked(raw) as string;
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
async function fetchPostMarkdown(slug: string): Promise<string> {
// fetch from DB / CMS
return `# Hello from ${slug}`;
}Dynamic Imports and Lazy Loading
Next.js re-exports next/dynamic, a thin wrapper around React.lazy with SSR control built in. Use it to split off components that are:
- Only visible after a user interaction (modals, drawers, tooltips)
- Below the fold (charts, comment sections)
- Large and route-specific
The ssr: false option is important for browser-only libraries (those that access window or document) — it prevents the server from even attempting to import them.
The loading prop provides a skeleton while the chunk downloads, preventing layout shift.
// app/dashboard/page.tsx
import dynamic from 'next/dynamic';
// Heavy chart library — only loaded when the chart is rendered
const RevenueChart = dynamic(
() => import('@/components/RevenueChart'),
{
loading: () => <p>Loading chart...</p>,
ssr: false, // recharts/d3 uses window internally
}
);
// Modal — only loaded when user clicks "Export"
const ExportModal = dynamic(
() => import('@/components/ExportModal')
);
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<RevenueChart />
<ExportModal />
</main>
);
}Tree Shaking: Named vs Default Imports
Webpack and Turbopack can eliminate unused code — but only when libraries are structured to allow it (ESM with named exports). Your import style also matters.
Importing an entire library namespace defeats tree shaking even for ESM libraries. Always prefer named imports targeting the specific function you need.
For libraries that still ship CommonJS (CJS) only, tree shaking is impossible. Check the analyzer: if you import one utility from a 300 KB CJS library, all 300 KB goes into your bundle. In that case, look for a lighter ESM alternative or copy just the function you need.
Use the sideEffects: false field in your own package's package.json to signal that all your modules are safe to tree-shake.
// BAD — pulls in the entire lodash CJS bundle (~70 KB gzipped)
import _ from 'lodash';
const result = _.groupBy(items, 'category');
// BETTER — lodash-es is ESM; named import is tree-shakeable
import { groupBy } from 'lodash-es';
const result = groupBy(items, 'category');
// BEST for simple cases — just write it yourself
function groupBy<T>(arr: T[], key: keyof T): Record<string, T[]> {
return arr.reduce(
(acc, item) => {
const group = String(item[key]);
(acc[group] ??= []).push(item);
return acc;
},
{} as Record<string, T[]>
);
}
const result = groupBy(items, 'category');Replacing Heavy Libraries with Lighter Alternatives
The single highest-impact optimization is often swapping a large library for a smaller one that does exactly what you need. Common replacements:
- moment (67 KB gz) → date-fns (tree-shakeable, ~3 KB per function) or dayjs (2 KB gz)
- axios (13 KB gz) → native
fetch(0 KB, built into Node 18+ and all browsers) - lodash (70 KB gz) → lodash-es + tree shaking, or native array methods
- highlight.js (full) → import only the languages you need via the slim build
The pattern below shows swapping moment for date-fns in a Server Action context where only one formatting function is needed.
// BEFORE — moment ships all locales by default
// import moment from 'moment';
// const label = moment(date).format('MMM D, YYYY');
// AFTER — date-fns: import exactly what you need
import { format } from 'date-fns';
// This Server Action formats a timestamp — runs only on the server
export async function getFormattedDate(
isoString: string
): Promise<string> {
'use server';
const date = new Date(isoString);
return format(date, 'MMM d, yyyy'); // e.g. "Jun 11, 2026"
}Measuring Real Bundle Impact with next build Output
You do not always need the full visual treemap. Next.js prints a route size table after every production build. Learn to read it:
- Size — JavaScript downloaded for that route specifically (route-unique chunks)
- First Load JS — total JS including shared chunks (what the user actually downloads on first visit)
The footer shows the shared chunk total. A green First Load JS is under 100 KB. Yellow is a warning. Red (over 500 KB) needs immediate attention.
Track this number in CI by failing the build when it exceeds a threshold, using the experimental.bundlePagesRouterDependencies or a custom size-limit script.
// scripts/check-bundle-size.ts
// Run after `next build` parses .next/build-manifest.json
import fs from 'node:fs';
import path from 'node:path';
const FIRST_LOAD_LIMIT_KB = 200;
interface BuildManifest {
pages: Record<string, string[]>;
}
function getFileSizeKb(filePath: string): number {
try {
const stats = fs.statSync(filePath);
return stats.size / 1024;
} catch {
return 0;
}
}
const manifestPath = path.join(process.cwd(), '.next/build-manifest.json');
const manifest: BuildManifest = JSON.parse(
fs.readFileSync(manifestPath, 'utf-8')
);
let exceeded = false;
for (const [page, files] of Object.entries(manifest.pages)) {
const totalKb = files.reduce((sum, f) => {
return sum + getFileSizeKb(path.join(process.cwd(), '.next', f));
}, 0);
if (totalKb > FIRST_LOAD_LIMIT_KB) {
console.error(`FAIL ${page}: ${totalKb.toFixed(1)} KB > ${FIRST_LOAD_LIMIT_KB} KB`);
exceeded = true;
}
}
if (exceeded) process.exit(1);
console.log('Bundle size check passed.');Using modularizeImports for Icon and UI Libraries
Icon libraries like react-icons and component libraries like @mui/material are notorious bundle killers when imported naively. Even a named import like import { FiTrash } from 'react-icons/fi' can pull in the entire icon set if the library's internal structure is not tree-shakeable.
Next.js 15 provides modularizeImports in next.config.ts to automatically rewrite barrel imports into direct deep imports at build time — no change needed in your component files.
This is equivalent to writing import FiTrash from 'react-icons/fi/index.js' but happens transparently, keeping your source code clean.
// next.config.ts
import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
const nextConfig: NextConfig = {
modularizeImports: {
// Rewrites: import { FiTrash } from 'react-icons/fi'
// To: import FiTrash from 'react-icons/fi/FiTrash'
'react-icons/?((<alpha>*))': {
transform: 'react-icons/{{ matches.[1] }}/{{ member }}',
},
// Same pattern for @mui/material
'@mui/material': {
transform: '@mui/material/{{ member }}',
},
'@mui/icons-material': {
transform: '@mui/icons-material/{{ member }}',
},
},
};
export default withBundleAnalyzer(nextConfig);Externalizing Server-Only Packages
Some packages should never reach the client bundle. Database drivers (pg, mysql2), encryption libraries (bcrypt), and file-system utilities are server-only by design. Next.js 15 provides two mechanisms to enforce this:
server-onlynpm package — addingimport 'server-only'to a module throws a build-time error if that module is ever imported in a Client Componentexperimental.serverComponentsExternalPackagesinnext.config.ts— tells the bundler to leave certain packages as Node.jsrequire()calls rather than bundling them
Using both together gives you belt-and-suspenders protection: the import guard catches mistakes in your code, and the external config handles native binaries that webpack cannot bundle anyway.
// lib/db.ts — database client, server-only
import 'server-only'; // build error if imported in a Client Component
import { Pool } from 'pg';
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
// These packages stay as require() — not bundled by webpack
// Needed for native addons like bcrypt, sharp, prisma engine
serverComponentsExternalPackages: ['bcrypt', 'sharp', '@prisma/client'],
},
};
export default nextConfig;Knowledge Check: Bundle Optimization Strategy
You run npm run analyze and discover that highlight.js (full build, ~900 KB parsed) appears in your client bundle. The syntax highlighting is only used on /blog/[slug] post pages to colorize code blocks. The code blocks are static — the same for every visitor. What is the best optimization strategy?
Recap: Analyzing and Shrinking the Client Bundle
In this lesson you learned a systematic approach to reducing the JavaScript your Next.js 15 app ships to users:
- Install and run @next/bundle-analyzer — wraps your build with an interactive treemap; triggered via
ANALYZE=true next build - Read the treemap — focus on gzipped size in the client report; look for large rectangles, server-only packages, and duplicate modules
- Move server-only work to Server Components — Markdown renderers, syntax highlighters, PDF parsers, and DB clients have no business in the browser
- Use next/dynamic for deferred loading — split off modals, charts, and below-the-fold components; use
ssr: falsefor browser-only packages - Tree shake correctly — prefer named ESM imports; replace CJS-only libraries with ESM alternatives
- Use modularizeImports — automatically deep-import icon and UI library members at build time
- Enforce server boundaries —
import 'server-only'andserverComponentsExternalPackagesprevent accidental client leakage
Run the analyzer before and after each optimization to confirm the impact. Automate a size budget check in CI to prevent regressions.
Domande Frequenti
La lezione «Analisi e riduzione del bundle client» è gratuita?
Sì — il testo completo di «Analisi e riduzione del bundle client» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Next.js 15 Fullstack (App Router + Server Actions), passa a CoddyKit PRO. Il corso Next.js 15 Fullstack (App Router + Server Actions) include 4 lezioni in totale.
Cosa imparerò in «Analisi e riduzione del bundle client»?
Usi il bundle analyzer per individuare le dipendenze pesanti e ridurre il JavaScript client inviato agli utenti. Eserciti Next.js 15 Fullstack (App Router + Server Actions) con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Next.js 15 Fullstack (App Router + Server Actions)?
Non è richiesta alcuna esperienza precedente. Next.js 15 Fullstack (App Router + Server Actions) su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «Analisi e riduzione del bundle client»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Next.js 15 Fullstack (App Router + Server Actions)?
Sì. Ogni lezione Next.js 15 Fullstack (App Router + Server Actions) include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Analisi e riduzione del bundle client
- Approfondimento sulla configurazione di Turbopack e del compilatore
- Confini dei moduli con server-only e client-only
- Import dinamici, code splitting e hydration differita