Bundle size optimization including tree shaking, code splitting, and dependency analysis. Use when reducing bundle size.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers techniques for optimizing JavaScript bundle size.
Use this skill when:
SHIP LESS JAVASCRIPT - Every kilobyte counts. Remove what you don't need, defer what you don't need immediately.
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
],
});
npm install -D @next/bundle-analyzer
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer(config);
ANALYZE=true npm run build
npx source-map-explorer dist/**/*.js
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App(): React.ReactElement {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
const HeavyChart = lazy(() => import('./components/HeavyChart'));
function Dashboard(): React.ReactElement {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'vendor-charts': ['recharts'],
},
},
},
},
// ❌ Imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ Imports only what's needed
import debounce from 'lodash/debounce';
debounce(fn, 300);
// ✅ Or use lodash-es for better tree shaking
import { debounce } from 'lodash-es';
// ❌ Imports all icons
import * as Icons from 'lucide-react';
// ✅ Imports specific icons
import { Home, Settings, User } from 'lucide-react';
// ❌ moment.js (330KB)
import moment from 'moment';
// ✅ date-fns (tree-shakeable)
import { format, parseISO } from 'date-fns';
// ✅ dayjs (2KB)
import dayjs from 'dayjs';
| Heavy Library | Size | Alternative | Size |
|---|---|---|---|
| moment.js | 330KB | date-fns | ~20KB |
| lodash | 530KB | lodash-es (specific) | ~5KB |
| axios | 40KB | fetch (native) | 0KB |
| uuid | 18KB | crypto.randomUUID() | 0KB |
| classnames | 1.8KB | clsx | 0.3KB |
async function loadPolyfill(): Promise<void> {
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}
}
async function loadAnalytics(): Promise<void> {
if (process.env.NODE_ENV === 'production') {
const { initAnalytics } = await import('./analytics');
initAnalytics();
}
}
const PDFViewer = lazy(() => import('./PDFViewer'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));
function MediaViewer({ type, src }: MediaViewerProps): React.ReactElement {
if (type === 'pdf') {
return (
<Suspense fallback={<Loading />}>
<PDFViewer src={src} />
</Suspense>
);
}
if (type === 'video') {
return (
<Suspense fallback={<Loading />}>
<VideoPlayer src={src} />
</Suspense>
);
}
return <img src={src} alt="" />;
}
// React Router
import { Link } from 'react-router-dom';
function Nav(): React.ReactElement {
const prefetchDashboard = (): void => {
import('./pages/Dashboard');
};
return (
<Link to="/dashboard" onMouseEnter={prefetchDashboard}>
Dashboard
</Link>
);
}
import Link from 'next/link';
// Automatic prefetching (default)
<Link href="/dashboard">Dashboard</Link>
// Disable prefetching
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
// tailwind.config.ts
export default {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
// Tailwind automatically purges unused styles in production
};
Use build-time extraction for CSS-in-JS:
// Use vanilla-extract for zero-runtime CSS
import { style } from '@vanilla-extract/css';
export const button = style({
backgroundColor: 'blue',
color: 'white',
});
// Use next/image for automatic optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For LCP images
placeholder="blur"
blurDataURL="..."
/>
// Or use vite-imagetools
import heroImage from './hero.jpg?w=1200&format=webp';
npm install -D size-limit @size-limit/preset-app
// package.json
{
"size-limit": [
{
"path": "dist/**/*.js",
"limit": "200 KB"
}
],
"scripts": {
"size": "size-limit",
"size-check": "size-limit --ci"
}
}
# .github/workflows/size.yml
- name: Check bundle size
run: npm run size-check
| Metric | Target | Good | Critical |
|---|---|---|---|
| Initial JS | < 100KB | < 150KB | > 200KB |
| Per-route JS | < 50KB | < 100KB | > 150KB |
| Total JS | < 500KB | < 750KB | > 1MB |