Bundle Analysis Skill
This skill helps you analyze and optimize JavaScript bundle sizes for Next.js and web applications.
When to Use This Skill
-
Large bundle sizes (>500KB)
-
Slow initial page loads
-
High First Contentful Paint (FCP)
-
Investigating bundle composition
-
Identifying duplicate dependencies
-
Optimizing production builds
-
Reducing Time to Interactive (TTI)
Bundle Size Goals
-
Initial bundle: <200KB (gzipped)
-
First Load JS: <300KB total
-
Route chunks: <100KB each
-
Vendor chunks: <150KB
-
Time to Interactive: <3s on 3G
Next.js Bundle Analyzer
Setup
Install bundle analyzer
pnpm add -D @next/bundle-analyzer
Or as dev dependency in web app
cd apps/web pnpm add -D @next/bundle-analyzer
Configuration
// apps/web/next.config.ts import type { NextConfig } from "next"; import withBundleAnalyzer from "@next/bundle-analyzer";
const bundleAnalyzer = withBundleAnalyzer({ enabled: process.env.ANALYZE === "true", });
const nextConfig: NextConfig = { // ... your config };
export default bundleAnalyzer(nextConfig);
Running Analysis
Analyze production bundle
cd apps/web ANALYZE=true pnpm build
Opens two HTML reports in browser:
- client.html: Client-side bundle
- server.html: Server-side bundle
Analyze specific environment
ANALYZE=true NODE_ENV=production pnpm build
Save report to file
ANALYZE=true pnpm build > bundle-report.txt
Reading Bundle Reports
Bundle Analyzer Output
Page Size First Load JS ┌ ○ / 5.2 kB 120 kB ├ /_app 0 B 115 kB ├ ○ /404 3.1 kB 118 kB ├ λ /api/cars 0 B 115 kB ├ ○ /blog 8.5 kB 128 kB ├ ○ /blog/[slug] 12.3 kB 132 kB └ ○ /charts 45.2 kB 165 kB
- First Load JS shared by all 115 kB ├ chunks/framework-[hash].js 42 kB ├ chunks/main-[hash].js 28 kB ├ chunks/pages/_app-[hash].js 35 kB └ chunks/webpack-[hash].js 10 kB
○ (Static) automatically rendered as static HTML λ (Server) server-side renders at runtime
Key Metrics:
-
Size: Page-specific JavaScript
-
First Load JS: Total JS loaded for initial render
-
Shared chunks: Code shared across pages
Interactive Report
Open client.html or server.html in browser:
-
Large boxes: Heavy dependencies
-
Colors: Different packages
-
Hover: See exact sizes
-
Click: Drill down into dependencies
Common Issues
- Large Dependencies
Find large packages
cd apps/web pnpm list --depth=0 | sort -k2 -n
Example output:
lodash 1.2 MB
moment 500 KB
recharts 300 KB
@heroui/react 250 KB
Solutions:
// ❌ Import entire library import _ from "lodash"; import moment from "moment";
// ✅ Import only what you need import debounce from "lodash/debounce"; import groupBy from "lodash/groupBy";
// ✅ Use lighter alternatives import { format } from "date-fns"; // Instead of moment import dayjs from "dayjs"; // Smaller than moment
- Duplicate Dependencies
Check for duplicates
pnpm list <package-name>
Example: Multiple versions of react
pnpm list react
Output:
@sgcarstrends/web
├── react@18.3.0
└─┬ some-package
└── react@18.2.0 # Duplicate!
Solutions:
// package.json { "pnpm": { "overrides": { "react": "18.3.0", "react-dom": "18.3.0" } } }
- Missing Code Splitting
// ❌ Import heavy component directly import Chart from "@/components/Chart";
export default function Page() { return <Chart data={data} />; }
// ✅ Dynamic import with code splitting import dynamic from "next/dynamic";
const Chart = dynamic(() => import("@/components/Chart"), { loading: () => <div>Loading chart...</div>, ssr: false, // Client-side only if needed });
export default function Page() { return <Chart data={data} />; }
- Large JSON/Data Files
// ❌ Import large JSON files import brands from "@/data/car-brands.json"; // 500KB
// ✅ Load dynamically export async function getBrands() { const response = await fetch("/api/brands"); return response.json(); }
// ✅ Or use dynamic import export async function getBrands() { const { default: brands } = await import("@/data/car-brands.json"); return brands; }
Optimization Techniques
- Tree Shaking
// ✅ Named imports enable tree shaking import { Button, Card } from "@heroui/react";
// ❌ Default import includes everything import HeroUI from "@heroui/react";
Verify tree shaking:
// package.json { "sideEffects": false // Enable aggressive tree shaking }
// Or specify side-effect files { "sideEffects": [".css", ".scss"] }
- Code Splitting by Route
// Next.js automatically code-splits by route // Each page becomes a separate chunk
// app/page.tsx -> chunk for / // app/blog/page.tsx -> chunk for /blog // app/charts/page.tsx -> chunk for /charts
// ✅ Additional manual splitting const HeavyComponent = dynamic(() => import("./HeavyComponent"));
- Lazy Loading
// ✅ Load components on interaction "use client";
import { useState } from "react"; import dynamic from "next/dynamic";
const CommentForm = dynamic(() => import("./CommentForm"));
export default function BlogPost() { const [showComments, setShowComments] = useState(false);
return ( <div> <article>{/* post content */}</article> <button onClick={() => setShowComments(true)}> Show Comments </button> {showComments && <CommentForm />} </div> ); }
- Optimize Dependencies
Replace heavy packages with lighter alternatives
❌ moment (500KB)
pnpm remove moment
✅ date-fns (30KB) or dayjs (7KB)
pnpm add date-fns
❌ lodash (full library)
✅ lodash-es (ESM with tree-shaking)
pnpm add lodash-es
❌ axios (for simple requests)
✅ native fetch or ky (12KB)
pnpm add ky
- Image Optimization
// ✅ Use Next.js Image component import Image from "next/image";
export default function Logo() { return ( <Image src="/logo.png" alt="Logo" width={200} height={100} priority // For above-fold images quality={75} // Reduce quality if acceptable /> ); }
// ✅ Use modern formats // - WebP: 25-35% smaller than JPEG/PNG // - AVIF: 50% smaller than JPEG (if supported)
- Font Optimization
// app/layout.tsx import { Inter } from "next/font/google";
// ✅ Load only needed weights and subsets const inter = Inter({ subsets: ["latin"], weight: ["400", "600", "700"], // Only load what you use display: "swap", preload: true, });
export default function RootLayout({ children }) { return ( <html lang="en" className={inter.className}> <body>{children}</body> </html> ); }
Advanced Analysis
Webpack Bundle Analyzer
For custom webpack configs
pnpm add -D webpack-bundle-analyzer
View detailed dependency tree
ANALYZE=true pnpm build
Source Map Explorer
Analyze source maps
pnpm add -D source-map-explorer
Generate production build with source maps
pnpm build
Analyze
npx source-map-explorer '.next/static/chunks/*.js'
Bundle Buddy
Visualize bundle relationships
npx bundle-buddy '.next/**/*.js.map'
Monitoring Bundle Size
CI Bundle Size Check
.github/workflows/bundle-size.yml
name: Bundle Size Check
on: pull_request: branches: [main]
jobs: bundle-size: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v4 with: node-version: 20 cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm -F @sgcarstrends/web build
- name: Analyze bundle
run: |
BUNDLE_SIZE=$(du -sh apps/web/.next/static | cut -f1)
echo "Bundle size: $BUNDLE_SIZE"
# Fail if bundle exceeds limit
SIZE_KB=$(du -sk apps/web/.next/static | cut -f1)
if [ $SIZE_KB -gt 500 ]; then
echo "Bundle size exceeds 500KB!"
exit 1
fi
- name: Comment PR
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '📦 Bundle size: ${{ env.BUNDLE_SIZE }}'
})
Bundle Size Tracking
Track bundle size over time
echo "$(date +%Y-%m-%d),$(du -sk apps/web/.next/static | cut -f1)" >> bundle-history.csv
Plot with gnuplot or spreadsheet
Performance Budgets
next.config.ts
// apps/web/next.config.ts const nextConfig: NextConfig = { // Warn if bundles exceed limits onDemandEntries: { maxInactiveAge: 25 * 1000, pagesBufferLength: 2, },
// Set performance budgets experimental: { optimizeCss: true, optimizePackageImports: ["@heroui/react", "recharts"], }, };
Lighthouse CI
.lighthouserc.json
{ "ci": { "collect": { "startServerCommand": "pnpm start", "url": ["http://localhost:3000", "http://localhost:3000/charts"] }, "assert": { "assertions": { "categories:performance": ["error", { "minScore": 0.9 }], "resource-summary:script:size": ["error", { "maxNumericValue": 300000 }], "total-byte-weight": ["error", { "maxNumericValue": 500000 }] } } } }
Best Practices
- Measure Before Optimizing
✅ Always measure first
ANALYZE=true pnpm build
Identify actual bottlenecks
Don't prematurely optimize
- Prioritize Critical Path
// ✅ Load critical resources first // Above-fold content, essential JS/CSS
// ❌ Don't block initial render // Defer analytics, chat widgets, etc.
- Use External CDN
// For heavy third-party libraries // Load from CDN instead of bundling
// next.config.ts const nextConfig: NextConfig = { experimental: { externalDir: true, }, };
- Regular Audits
✅ Run bundle analysis regularly
- Before each release
- When adding dependencies
- After major refactors
ANALYZE=true pnpm build
Troubleshooting
Bundle Size Not Reducing
Issue: Optimizations not working
Solution: Check production build
Ensure building for production
NODE_ENV=production pnpm build
Verify minification
cat .next/static/chunks/main-*.js | head -n 1
Should be minified (single line)
Analyzer Not Opening
Issue: Bundle analyzer not showing reports
Solution: Check environment variable
Ensure ANALYZE is set
echo $ANALYZE # Should print "true"
Run explicitly
cd apps/web ANALYZE=true pnpm build
Check for errors in build output
Large Unexplained Bundle
Issue: Bundle larger than expected
Solution: Investigate with source-map-explorer
pnpm build npx source-map-explorer '.next/static/chunks/*.js' --html bundle-report.html
Open bundle-report.html to see breakdown
References
-
Next.js Bundle Analyzer: https://www.npmjs.com/package/@next/bundle-analyzer
-
Webpack Bundle Analyzer: https://github.com/webpack-contrib/webpack-bundle-analyzer
-
Web.dev Bundle Size: https://web.dev/your-first-performance-budget/
-
Next.js Optimization: https://nextjs.org/docs/app/building-your-application/optimizing
-
Related files:
-
apps/web/next.config.ts
-
Next.js configuration
-
Root CLAUDE.md - Performance guidelines
Best Practices Summary
-
Analyze Regularly: Check bundle size before releases
-
Code Split: Use dynamic imports for heavy components
-
Tree Shake: Use named imports, mark side effects
-
Optimize Dependencies: Replace heavy packages with lighter alternatives
-
Lazy Load: Defer non-critical resources
-
Monitor: Set up CI checks and performance budgets
-
Use CDN: Offload heavy third-party libraries
-
Image Optimization: Use Next.js Image with WebP/AVIF