Skip to content

Bundle Analysis & Size Budgets

FieldValue
TypeAgent Reference
Source~/.copilot/agents/_refs/react-perf-auditor/bundle-analysis.md
DescriptionNot specified

Source Content

Bundle Analysis & Size Budgets

Visualizing the Bundle

rollup-plugin-visualizer (Vite / Astro)

Terminal window
pnpm add -D rollup-plugin-visualizer
// vite.config.ts or astro.config.ts vite override
import { visualizer } from 'rollup-plugin-visualizer'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
visualizer({
open: true, // auto-open after build
filename: 'dist/stats.html',
gzipSize: true,
brotliSize: true,
template: 'treemap', // or 'sunburst', 'network'
}),
],
})
Terminal window
pnpm build
# Opens dist/stats.html automatically — treemap of every module

What to look for in the treemap:

  • Unexpectedly large third-party libraries (moment.js, lodash, date-fns full build)
  • Duplicate libraries (two versions of React, two copies of Zod)
  • Entire icon sets imported instead of individual icons
  • Dev-only code leaking into production

bundlephobia

Check individual packages before adding them:

  • Visit bundlephobia.com/<package> to see minified + gzipped size
  • Check “Similar packages” for lighter alternatives
  • Check “Exports analysis” to confirm tree-shaking works

Rule: Do not add a package that adds > 20kb gzipped unless there is no lighter alternative and the functionality is essential.

Common Bloat Sources & Fixes

Full icon library import

// BAD — imports the entire Lucide library (~500kb)
import * as Icons from 'lucide-react'
const { Plus, Trash } = Icons
// GOOD — named import → tree-shaken to only Plus and Trash
import { Plus, Trash } from 'lucide-react'

Lodash

// BAD — imports all of lodash
import _ from 'lodash'
_.debounce(fn, 300)
// GOOD — import only the function
import debounce from 'lodash/debounce'
// BETTER — use native or tiny alternatives
// debounce: write a 5-line hook
// groupBy: Array.reduce
// cloneDeep: structuredClone()
// merge: { ...a, ...b }

date-fns

// date-fns v3+ is fully tree-shakeable — named imports are fine
import { format, parseISO, differenceInDays } from 'date-fns'

Moment.js — never use

// BAD — moment.js is 300kb+ and not tree-shakeable
import moment from 'moment'
// GOOD alternatives
import { format } from 'date-fns' // fully tree-shakeable
import { Temporal } from '@js-temporal/polyfill' // modern TC39 API

Setting CI Bundle Budgets

Use vite-plugin-bundlesize or a custom script to fail CI when bundle exceeds budget:

Terminal window
pnpm add -D vite-bundle-visualizer

Or use a custom script with rollup-plugin-visualizer JSON output:

scripts/check-bundle-size.mjs
import { readFileSync } from 'fs'
const stats = JSON.parse(readFileSync('./dist/stats.json', 'utf-8'))
const BUDGETS = {
// Total JS budget (gzipped)
totalGzip: 200 * 1024, // 200kb
// Individual chunk budgets (gzipped)
mainChunk: 80 * 1024, // 80kb
// Vendor chunk
vendorChunk: 100 * 1024, // 100kb
}
let failed = false
for (const [name, budget] of Object.entries(BUDGETS)) {
const actual = getChunkSize(stats, name)
if (actual > budget) {
console.error(`${name}: ${kb(actual)} > budget ${kb(budget)}`)
failed = true
} else {
console.log(`${name}: ${kb(actual)}${kb(budget)}`)
}
}
if (failed) process.exit(1)
package.json
{
"scripts": {
"bundle:check": "node scripts/check-bundle-size.mjs",
"build:check": "pnpm build && pnpm bundle:check"
}
}

CI step:

- name: Check bundle size
run: pnpm build:check

Vite Build Optimization

vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
// Split vendor libraries into a separate chunk for better caching
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'tanstack': ['@tanstack/react-query', '@tanstack/react-router', '@tanstack/react-table'],
'design-system': ['@dmwd-io/design-system'],
},
},
},
// Target modern browsers — smaller output
target: 'es2022',
// Enable minification
minify: 'esbuild',
// Enable source maps for production debugging (upload to error tracking, don't serve publicly)
sourcemap: true,
},
})

Dependency Audit

Regular checks:

Terminal window
# Find unused dependencies
npx depcheck
# Find outdated packages with security issues
pnpm audit
# Find packages with newer versions
pnpm outdated

When to remove a dependency:

  • It’s unused (depcheck confirms)
  • Its functionality can be replaced with < 20 lines of native code
  • A much lighter alternative exists (e.g., replace axios with native fetch)
  • It was added for a feature that was later removed

Module Analysis in devtools

Chrome DevTools → Coverage tab:

  1. Open DevTools → More tools → Coverage
  2. Record → interact with the page → stop
  3. Look for JS files with > 50% unused code (shown in red)
  4. Those files are candidates for lazy loading or removal