Claude-skill-registry frontend-bundle-analysis-and-optimization
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/frontend-bundle-analysis-and-optimization" ~/.claude/skills/majiayu000-claude-skill-registry-frontend-bundle-analysis-and-optimization && rm -rf "$T"
skills/data/frontend-bundle-analysis-and-optimization/SKILL.mdFrontend Bundle Analysis and Optimization
This skill provides actionable strategies for analyzing and reducing JavaScript bundle size using Webpack or Turbopack, focusing on code splitting, tree shaking, and dependency analysis.
Bundle Analysis with @next/bundle-analyzer
Visual bundle analysis is the first step in any optimization effort. It shows which modules contribute most to bundle size.
Setup
npm install @next/bundle-analyzer --save-dev
// next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) module.exports = withBundleAnalyzer({ // Your Next.js config })
Usage
ANALYZE=true npm run build
This opens an interactive treemap showing:
- Which dependencies are largest
- Duplicate packages across chunks
- Opportunities for code splitting
Reading the Treemap
- Large blocks: Heavy dependencies (consider alternatives or lazy loading)
- Duplicate colors: Same package in multiple chunks (configure splitChunks)
- Vendor chunks: Third-party libraries (good candidates for caching)
Dynamic Imports
Dynamic imports enable code splitting, loading JavaScript only when needed.
Component-Based Code Splitting
// BAD: Loads heavy component immediately import HeavyChart from '@/components/HeavyChart' export default function Dashboard() { return <HeavyChart data={data} /> } // GOOD: Loads only when rendered import dynamic from 'next/dynamic' const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { loading: () => <ChartSkeleton />, ssr: false, // Disable SSR if component uses browser APIs }) export default function Dashboard() { return <HeavyChart data={data} /> }
Conditional Loading
// Load component only when needed import dynamic from 'next/dynamic' import { useState } from 'react' const AdminPanel = dynamic(() => import('@/components/AdminPanel')) export default function Dashboard({ isAdmin }: { isAdmin: boolean }) { const [showAdmin, setShowAdmin] = useState(false) return ( <div> <h1>Dashboard</h1> {isAdmin && ( <button onClick={() => setShowAdmin(true)}> Show Admin Panel </button> )} {/* Only loads when button is clicked */} {showAdmin && <AdminPanel />} </div> ) }
Named Exports
// For named exports, use an object with 'default' const DynamicComponent = dynamic(() => import('@/components/Hello').then((mod) => mod.Hello) )
Multiple Components
// BAD: Import entire library import { ComponentA, ComponentB } from 'heavy-library' // GOOD: Split into separate chunks const ComponentA = dynamic(() => import('heavy-library/ComponentA')) const ComponentB = dynamic(() => import('heavy-library/ComponentB'))
With Custom Loading State
const HeavyEditor = dynamic(() => import('@/components/RichTextEditor'), { loading: () => ( <div className="editor-skeleton"> <div className="toolbar-skeleton" /> <div className="content-skeleton" /> </div> ), ssr: false, })
Route-Based Code Splitting
Next.js automatically code-splits by route, but you can optimize further.
Lazy Route Components
// app/dashboard/page.tsx import dynamic from 'next/dynamic' // Heavy components loaded only on this route const Analytics = dynamic(() => import('@/components/Analytics')) const UserTable = dynamic(() => import('@/components/UserTable')) export default function DashboardPage() { return ( <div> <Analytics /> <UserTable /> </div> ) }
Shared Layouts
// app/layout.tsx // Common layout code shared across routes export default function RootLayout({ children }) { return ( <html> <body> <Header /> {/* Shared, in main bundle */} {children} {/* Route-specific, code-split */} <Footer /> {/* Shared, in main bundle */} </body> </html> ) }
Webpack splitChunks Configuration
Fine-tune how code is divided into chunks for optimal caching and loading.
Default Next.js Configuration
Next.js has sensible defaults, but you can customize:
// next.config.js module.exports = { webpack: (config, { isServer }) => { if (!isServer) { config.optimization.splitChunks = { chunks: 'all', cacheGroups: { default: false, vendors: false, // Vendor chunk for node_modules vendor: { name: 'vendor', chunks: 'all', test: /node_modules/, priority: 20, }, // Common chunk for shared code common: { name: 'common', minChunks: 2, chunks: 'all', priority: 10, reuseExistingChunk: true, enforce: true, }, }, } } return config }, }
Framework Chunk
// Separate React/Next.js into their own chunk for better caching cacheGroups: { framework: { name: 'framework', test: /[\\/]node_modules[\\/](react|react-dom|next)[\\/]/, priority: 40, enforce: true, }, }
Library-Specific Chunks
// Create separate chunk for a heavy library cacheGroups: { charts: { name: 'charts', test: /[\\/]node_modules[\\/](recharts|d3|chart\.js)[\\/]/, priority: 30, enforce: true, }, }
Size-Based Splitting
splitChunks: { chunks: 'all', maxSize: 244 * 1024, // 244 KB max chunk size minSize: 20 * 1024, // 20 KB min chunk size }
Tree Shaking and Side Effects
Tree shaking eliminates unused code, but requires proper configuration.
Package.json sideEffects
{ "name": "my-library", "sideEffects": false }
This tells bundlers that no files have side effects, enabling aggressive tree shaking.
Specific Side Effects
{ "sideEffects": [ "*.css", "*.scss", "./src/polyfills.ts" ] }
Import Best Practices
// BAD: Imports entire library import _ from 'lodash' const result = _.debounce(fn, 100) // GOOD: Import specific function import debounce from 'lodash/debounce' const result = debounce(fn, 100) // EVEN BETTER: Use tree-shakeable alternative import { debounce } from 'lodash-es' const result = debounce(fn, 100)
Named Imports
// BAD: Entire date-fns library loaded import * as dateFns from 'date-fns' dateFns.format(new Date(), 'yyyy-MM-dd') // GOOD: Only format function loaded import { format } from 'date-fns' format(new Date(), 'yyyy-MM-dd')
Barrel File Issues
// utils/index.ts - BAD: Barrel file prevents tree shaking export * from './stringUtils' export * from './arrayUtils' export * from './dateUtils' // GOOD: Import directly from specific files import { formatString } from '@/utils/stringUtils'
Turbopack Considerations
Turbopack is Next.js's Rust-based bundler, significantly faster than Webpack.
Enable for Development
next dev --turbo
Current Status (as of Next.js 15)
- Development: Stable, recommended
- Production: Experimental, opt-in
Enable for Production (Experimental)
// next.config.js module.exports = { experimental: { turbo: {}, }, }
Benefits
- 5-10x faster cold starts in development
- Faster HMR (Hot Module Replacement)
- Lower memory usage
Limitations
- Some Webpack plugins not yet supported
- Custom Webpack config requires migration
Migration Strategy
- Test in development first:
next dev --turbo - Verify all features work correctly
- Check for unsupported Webpack plugins
- Migrate custom configurations
- Test production builds thoroughly before deploying
Dependency Analysis
Find Heavy Dependencies
# Install dependency analyzer npm install -g depcheck # Find unused dependencies depcheck # Check bundle impact npm install -g bundle-phobia-cli bundle-phobia lodash
Lighter Alternatives
| Heavy Library | Lighter Alternative | Savings |
|---|---|---|
| moment.js (288 KB) | date-fns (27 KB) | 261 KB |
| lodash (72 KB) | lodash-es (tree-shakeable) | ~50 KB |
| axios (13 KB) | fetch API (native) | 13 KB |
| jquery (87 KB) | Native DOM APIs | 87 KB |
next/third-parties
Optimize third-party scripts:
// BAD: Blocks rendering <script src="https://www.googletagmanager.com/gtag/js" /> // GOOD: Optimized loading with next/third-parties import { GoogleAnalytics } from '@next/third-parties/google' export default function RootLayout({ children }) { return ( <html> <body>{children}</body> <GoogleAnalytics gaId="G-XYZ" /> </html> ) }
Performance Budgets
Set limits to prevent bundle size regression.
Next.js Performance Budgets
// next.config.js module.exports = { webpack: (config) => { config.performance = { maxAssetSize: 250 * 1024, // 250 KB maxEntrypointSize: 400 * 1024, // 400 KB hints: 'error', // Fail build if exceeded } return config }, }
CI Integration
// package.json { "scripts": { "build": "next build", "analyze": "ANALYZE=true next build", "check-bundle": "npm run build && bundlesize" }, "bundlesize": [ { "path": ".next/static/chunks/*.js", "maxSize": "250 KB" } ] }
Anti-Patterns
No Code Splitting
// BAD: Everything in one bundle import HeavyChart from './HeavyChart' import HeavyEditor from './HeavyEditor' import HeavyMap from './HeavyMap' export default function Page() { return ( <> <HeavyChart /> <HeavyEditor /> <HeavyMap /> </> ) }
Importing Entire Libraries
// BAD: 100+ KB import _ from 'lodash' // GOOD: ~5 KB import debounce from 'lodash/debounce'
Too Many Small Chunks
// BAD: Creates hundreds of tiny chunks splitChunks: { minSize: 1 * 1024, // 1 KB minimum } // GOOD: Reasonable chunk sizes splitChunks: { minSize: 20 * 1024, // 20 KB minimum }
No Bundle Analysis
Build without ever checking bundle composition. Always use bundle analyzer periodically.
Ignoring Duplicate Dependencies
Multiple versions of the same package across chunks. Use
npm dedupe or manage peer dependencies properly.
Optimization Checklist
- Bundle analyzer configured and reviewed regularly
- Dynamic imports for heavy components
- Route-based code splitting utilized
- splitChunks configured for optimal caching
- sideEffects configured in package.json
- Direct imports from specific modules (not barrel files)
- Lighter alternatives evaluated for heavy dependencies
- next/third-parties used for external scripts
- Performance budgets set and enforced
- Bundle size monitored in CI/CD
- Turbopack tested in development
Bundle Size Targets
- Initial JS load: < 170 KB (compressed)
- Total page weight: < 1 MB
- Largest chunk: < 250 KB
- Number of chunks: < 20 for most routes