12.4 · difficulty 2/4 · 10 min read
Bundle Analysis
Use rollup-plugin-visualizer to find the heavyweights in your bundle — which packages take up the most space, where you can optimize, and how to continuously track output size.
Visualizing your bundle
pnpm add -D rollup-plugin-visualizerimport { visualizer } from "rollup-plugin-visualizer"
export default defineConfig({
plugins: [
visualizer({
open: true, // Automatically open the browser after build
gzipSize: true, // Show gzip-compressed size
brotliSize: true, // Show brotli-compressed size
filename: "stats.html",
template: "treemap", // "treemap" | "sunburst" | "network"
}),
],
})How to read the visualization report
Treemap view (recommended): block area represents file size
┌─────────────────────────────────┐
│ react-dom (120KB) │
│ ┌───────────────────────────┐ │
│ │ react-dom.development.js │ │
│ └───────────────────────────┘ │
├─────────────────────────────────┤
│ antd (245KB) ← possibly too large! │
├─────────────────────────────────┤
│ lodash-es (8KB) │
└─────────────────────────────────┘Things to look for:
- The largest blocks: worth optimizing
- Unexpected packages: may be phantom dependencies
- Duplicate packages: the same package bundled twice
Common optimization strategies
Strategy 1: Switch to a lighter alternative
lodash → lodash-es (tree-shakeable on demand)
moment → dayjs (2KB vs 300KB)
axios → ky or fetch (native browser API)
antd → import specific components on demandStrategy 2: Lazy-load with dynamic import
// Defer loading the chart library until the user actually needs it
const ChartComponent = lazy(() => import("./heavyChartComponent"))Strategy 3: Check for dev dependencies leaking into production
// Make sure dev tooling doesn't end up in the production bundle
import { debug } from "debug" // ← Are you using this in production code?Monitoring bundle size in CI
Use bundlesize or size-limit to set size budgets in CI:
pnpm add -D size-limit @size-limit/preset-app{
"size-limit": [
{
"path": "dist/assets/*.js",
"limit": "300 KB" // CI fails if this is exceeded
}
]
}Or use the GitHub Actions preinstall-size-comment action to automatically post bundle size changes as PR comments.
Tie The Report To User Flows
The biggest rectangle in a bundle report is not always the first thing to optimize. Ask whether it is on a critical user path:
| Case | What to do |
|---|---|
| Large package in the initial entry chunk | Prioritize splitting or replacement |
| Large package only on an admin page | Lazy-load it; replacement may not be necessary |
| Large package is a chart/editor/map feature | Wrap the feature entry in dynamic import |
| Large package is framework runtime | First check for duplicate copies |
| Large package is polyfill/legacy output | Revisit target and compatibility strategy |
Do not over-split just to make the treemap look tidy. Too many chunks increase request, cache, and preload complexity. The goal is "faster critical paths", not "every rectangle is small".
Set Bundle Budgets
Budgets work best when separated:
- Initial JS: scripts required for the first route.
- Initial CSS: render-blocking styles.
- Single async chunk: editor, chart, map, and similar features.
- Total output size: catches accidental dependency growth.
For each budget, specify gzip or brotli and whether the number refers to modern or legacy output. Otherwise CI numbers become a source of debate instead of feedback.
Self-check
- In a bundle visualization tool, what does the area of a block represent?
- How do you tell whether
antdis pulling in unnecessary components? - What is a "phantom dependency"? How do you spot one in bundle analysis?
- Which better represents the actual download size a user experiences —
gzipSizeor the raw file size?
// Your bundle analysis shows:
// - @mui/material: 150KB (you only use Button and TextField)
// - recharts: 80KB (you only use LineChart)
// - date-fns: 45KB (you only use 3 functions)
//
// Write an optimization plan for each package:
// 1. On-demand imports for @mui/material
// 2. Lazy loading for recharts
// 3. The correct import style for date-fns
// 1. @mui/material on-demand import (avoid importing the full bundle)
// Wrong:
import { Button, TextField } from "@mui/material"
// Correct:
// TODO
// 2. Lazy loading for recharts
// Wrong:
import { LineChart } from "recharts"
// Correct:
// TODO
// 3. On-demand import for date-fns
// Wrong:
import { format, addDays, subMonths } from "date-fns"
// Correct:
// TODO