Bundle Analysis & Size Budgets
| Field | Value |
|---|---|
| Type | Agent Reference |
| Source | ~/.copilot/agents/_refs/react-perf-auditor/bundle-analysis.md |
| Description | Not specified |
Source Content
Bundle Analysis & Size Budgets
Visualizing the Bundle
rollup-plugin-visualizer (Vite / Astro)
pnpm add -D rollup-plugin-visualizer// vite.config.ts or astro.config.ts vite overrideimport { 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' }), ],})pnpm build# Opens dist/stats.html automatically — treemap of every moduleWhat 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 Trashimport { Plus, Trash } from 'lucide-react'Lodash
// BAD — imports all of lodashimport _ from 'lodash'_.debounce(fn, 300)
// GOOD — import only the functionimport 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 fineimport { format, parseISO, differenceInDays } from 'date-fns'Moment.js — never use
// BAD — moment.js is 300kb+ and not tree-shakeableimport moment from 'moment'
// GOOD alternativesimport { format } from 'date-fns' // fully tree-shakeableimport { Temporal } from '@js-temporal/polyfill' // modern TC39 APISetting CI Bundle Budgets
Use vite-plugin-bundlesize or a custom script to fail CI when bundle exceeds budget:
pnpm add -D vite-bundle-visualizerOr use a custom script with rollup-plugin-visualizer JSON output:
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){ "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:checkVite Build Optimization
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:
# Find unused dependenciesnpx depcheck
# Find outdated packages with security issuespnpm audit
# Find packages with newer versionspnpm outdatedWhen 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:
- Open DevTools → More tools → Coverage
- Record → interact with the page → stop
- Look for JS files with > 50% unused code (shown in red)
- Those files are candidates for lazy loading or removal