Bundle Optimization Skill
Overview
This project achieved a 90% bundle reduction (2.33MB → 236KB) through strategic optimizations. This skill documents proven patterns for maintaining optimal bundle size.
Key Achievement: Icon Optimization
Problem: Wildcard imports caused massive bundle bloat
Solution: Icon manifest with build-time generation
Result: 90% reduction in bundle size
The Problem
typescript
1// ❌ WRONG - Imports entire simple-icons library (2.33MB)
2import * as Icons from 'simple-icons';
3
4// This single line imports 2,000+ icons even if you only use 5
5const reactIcon = Icons.siReact;
6```typescript
7
8### The Solution: Icon Manifest
9
10**Location:** `src/lib/icon-manifest.ts`
11
12### How it works
13
141. Curate list of needed icons (42 in this project)
152. Build script generates manifest with only those icons
163. Runtime imports from manifest instead of full library
17
18```typescript
19// ✅ CORRECT - Only includes curated icons
20import { getIcon } from '@/lib/icon-manifest';
21
22const reactIcon = getIcon('react');
23// Bundle only includes 42 icons instead of 2,000+
24```typescript
25
26### Implementation
27
28**1. Icon Manifest Generator Script**
29
30**Location:** `scripts/generate-icons.js`
31
32```javascript
33// Runs during build process
34// Reads curated icon list
35// Generates optimized manifest
36// Only includes icons actually used in project
37```typescript
38
39**2. Icon Manifest**
40
41**Location:** `src/lib/icon-manifest.ts`
42
43```typescript
44// Auto-generated during build
45import {
46 siReact,
47 siTypescript,
48 siNextdotjs,
49 // ... only 42 icons
50} from 'simple-icons';
51
52const iconManifest = {
53 react: siReact,
54 typescript: siTypescript,
55 nextdotjs: siNextdotjs,
56 // ...
57};
58
59export const getIcon = (name: string) => iconManifest[name];
60```typescript
61
62**3. Package.json Build Hook**
63
64```json
65{
66 "scripts": {
67 "prebuild": "node scripts/generate-icons.js",
68 "build": "next build"
69 }
70}
71```typescript
72
73### Usage Pattern
74
75```typescript
76// Import the manifest function
77import { getIcon } from '@/lib/icon-manifest';
78
79function SkillIcon({ name }: { name: string }) {
80 const icon = getIcon(name);
81
82 if (!icon) return null;
83
84 return (
85 <div dangerouslySetInnerHTML={{ __html: icon.svg }} />
86 );
87}
88```typescript
89
90### Adding New Icons
91
921. Add icon name to curated list in `scripts/generate-icons.js`
932. Run build or `node scripts/generate-icons.js`
943. Manifest automatically regenerates
954. Use with `getIcon('newIconName')`
96
97## Tree-Shaking Strategies
98
99### 1. Named Imports Only
100
101```typescript
102// ✅ CORRECT - Tree-shakeable
103import { Button } from '@/components/ui/button';
104import { Camera, Settings } from 'lucide-react';
105
106// ❌ WRONG - Imports everything
107import * as Components from '@/components';
108import * as Icons from 'lucide-react';
109```typescript
110
111### 2. Lucide Icons Configuration
112
113**Location:** `next.config.ts`
114
115```typescript
116modularizeImports: {
117 'lucide-react': {
118 transform: 'lucide-react/dist/esm/icons/{{kebabCase member}}',
119 skipDefaultConversion: true,
120 },
121}
122```typescript
123
124**Effect:** Each Lucide icon imports only its file, not entire library
125
126### 3. Dynamic Imports for Large Dependencies
127
128```typescript
129// ✅ CORRECT - Load only when needed
130const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
131 loading: () => <Skeleton />,
132 ssr: false, // Skip SSR if not needed
133});
134
135// Use in component
136<HeavyChart data={data} />
137```typescript
138
139### 4. Code Splitting by Route
140
141Next.js automatically code-splits by route, but you can optimize:
142
143```typescript
144// app/heavy-feature/page.tsx
145import dynamic from 'next/dynamic';
146
147// Split heavy components
148const HeavyFeature = dynamic(() => import('@/components/heavy-feature'));
149
150export default function Page() {
151 return <HeavyFeature />;
152}
153```typescript
154
155## Bundle Analysis Workflow
156
157### Step 1: Enable Bundle Analyzer
158
159```bash
160# Install analyzer
161npm install --save-dev @next/bundle-analyzer
162
163# Run analysis
164ANALYZE=true npm run build
165```typescript
166
167### Step 2: Review Results
168
169Analyzer opens in browser showing:
170
171- Chunk sizes
172- Import paths
173- Duplicate dependencies
174- Optimization opportunities
175
176### Step 3: Identify Issues
177
178### Look for
179
180- Large chunks (>500KB)
181- Duplicate libraries (same lib in multiple chunks)
182- Unused imports
183- Large dependencies that could be lazy-loaded
184
185### Step 4: Optimize
186
187### Common fixes
188
1891. Convert wildcard imports to named imports
1902. Add dynamic imports for heavy components
1913. Remove unused dependencies from package.json
1924. Use smaller alternatives (e.g., date-fns instead of moment)
193
194## Size Limit Configuration
195
196**Location:** `package.json`
197
198```json
199{
200 "size-limit": [
201 {
202 "path": ".next/static/chunks/app/page.js",
203 "limit": "40 KB",
204 "name": "Homepage"
205 },
206 {
207 "path": ".next/static/chunks/app/skills/page.js",
208 "limit": "40 KB",
209 "name": "Skills Page"
210 }
211 ]
212}
213```typescript
214
215### Running Size Checks
216
217```bash
218# Check all size limits
219npm run size
220
221# Output shows:
222# ✓ Homepage: 7.73 KB / 40 KB (within limit)
223# ✓ Skills: 6.31 KB / 40 KB (within limit)
224```typescript
225
226### CI/CD Integration
227
228Size limits run in GitHub Actions quality gates:
229
230```yaml
231- name: Check bundle size
232 run: npm run size
233```typescript
234
235**Failure:** Blocks PR if any bundle exceeds limit
236
237## Performance Optimization Patterns
238
239### 1. Image Optimization
240
241```typescript
242// ✅ CORRECT - Next.js Image component
243import Image from 'next/image';
244
245<Image
246 src="/assets/photo.jpg"
247 alt="Description"
248 width={800}
249 height={600}
250 priority={false} // Lazy load by default
251 quality={85}
252/>
253```typescript
254
255### Benefits
256
257- Automatic WebP/AVIF conversion
258- Lazy loading by default
259- Responsive images
260- Optimized serving
261
262### 2. Font Optimization
263
264```typescript
265// app/layout.tsx
266import { Inter } from 'next/font/google';
267
268const inter = Inter({
269 subsets: ['latin'],
270 display: 'swap',
271 preload: true,
272});
273
274export default function RootLayout({ children }) {
275 return (
276 <html className={inter.className}>
277 <body>{children}</body>
278 </html>
279 );
280}
281```typescript
282
283### Benefits
284
285- Self-hosted fonts (no external requests)
286- Automatic font optimization
287- Zero layout shift
288
289### 3. Script Loading Strategies
290
291```typescript
292import Script from 'next/script';
293
294// Load after page is interactive
295<Script
296 src="https://analytics.example.com/script.js"
297 strategy="lazyOnload"
298/>
299
300// Load before page is interactive
301<Script
302 src="https://critical.example.com/script.js"
303 strategy="beforeInteractive"
304/>
305```typescript
306
307### 4. CSS Optimization
308
309### Tailwind Configuration
310
311```javascript
312// tailwind.config.js
313module.exports = {
314 content: [
315 './src/**/*.{js,ts,jsx,tsx,mdx}',
316 ],
317 // Purges unused CSS in production
318};
319```typescript
320
321**Result:** Only CSS actually used is included in bundle
322
323## Monitoring Bundle Size
324
325### Current Metrics (Production)
326
327```typescript
328Homepage: 7.73 KB / 40 KB (19% of limit) ✅
329Skills Page: 6.31 KB / 40 KB (16% of limit) ✅
330Shared: 102 KB (routes share this)
331```typescript
332
333### Bundle Size History
334
335### Before Optimization (October 2024)
336
337- Homepage: 2.33 MB (with wildcard icon import)
338- Skills Page: 2.35 MB
339
340### After Icon Manifest (October 2024)
341
342- Homepage: 236 KB (90% reduction)
343- Skills Page: 193 KB
344
345### After Additional Optimizations (November 2024)
346
347- Homepage: 7.73 KB
348- Skills Page: 6.31 KB
349
350### Tracking Over Time
351
352```bash
353# Run before making changes
354npm run size > before.txt
355
356# Make optimizations
357
358# Run after changes
359npm run size > after.txt
360
361# Compare
362diff before.txt after.txt
363```typescript
364
365## Common Optimization Mistakes
366
367### Mistake 1: Over-Splitting
368
369```typescript
370// ❌ WRONG - Too aggressive splitting
371const Button = dynamic(() => import('./button'));
372const Text = dynamic(() => import('./text'));
373const Icon = dynamic(() => import('./icon'));
374
375// Network overhead > bundle savings
376```typescript
377
378**Fix:** Only split genuinely large components (>50KB)
379
380### Mistake 2: Importing Dev Dependencies in Production
381
382```typescript
383// ❌ WRONG - Imports dev tool in production
384import { faker } from '@faker-js/faker';
385
386const mockData = faker.name.firstName();
387```typescript
388
389**Fix:** Use conditional imports or environment checks
390
391```typescript
392// ✅ CORRECT
393const mockData = process.env.NODE_ENV === 'development'
394 ? await import('@faker-js/faker').then(m => m.faker.name.firstName())
395 : 'John';
396```typescript
397
398### Mistake 3: Duplicate Dependencies
399
400```bash
401# Check for duplicates
402npm ls <package-name>
403
404# Example: Multiple versions of same package
405npm ls react
406# ├── react@19.0.0
407# └── some-lib
408# └── react@18.0.0 # ❌ Duplicate!
409```typescript
410
411**Fix:** Update dependencies to use same version
412
413## Performance Budget
414
415### Current Limits
416
417- **Homepage:** 40 KB (first-party JavaScript)
418- **Other Routes:** 40 KB each
419- **Shared Chunks:** 150 KB total
420- **Images:** Lazy-loaded, optimized format
421
422### Adding New Features
423
424### Before adding dependency
425
4261. Check package size on Bundlephobia
4272. Consider alternatives
4283. Run size check after installation
4294. Verify no significant increase
430
431```bash
432# Check package size before installing
433npx bundlephobia <package-name>
434
435# Install
436npm install <package-name>
437
438# Verify size impact
439npm run size
440```typescript
441
442## Optimization Checklist
443
444When adding new features:
445
446- [ ] Use named imports only (no wildcards)
447- [ ] Check if dependency can be dynamic import
448- [ ] Verify icon added to manifest (if using icons)
449- [ ] Run bundle analyzer to check impact
450- [ ] Ensure size limits still pass
451- [ ] Test production build size
452- [ ] Consider server-side alternative
453- [ ] Check for lighter alternatives
454
455## Quick Reference Commands
456
457```bash
458# Bundle analysis
459ANALYZE=true npm run build
460
461# Size limit check
462npm run size
463
464# Check package size before install
465npx bundlephobia <package>
466
467# Find duplicate dependencies
468npm ls <package>
469
470# Production build test
471npm run build && npm run start
472
473# Generate icon manifest
474node scripts/generate-icons.js
475```typescript
476
477## Related Files
478
479- `scripts/generate-icons.js` - Icon manifest generator
480- `src/lib/icon-manifest.ts` - Generated icon manifest
481- `next.config.ts` - Lucide tree-shaking configuration
482- `package.json` - Size limit configuration
483- `.github/workflows/quality-gates.yml` - CI/CD size checks
484
485## Resources
486
487- [Bundlephobia](https://bundlephobia.com/) - Check package sizes
488- [Next.js Bundle Analyzer](https://www.npmjs.com/package/@next/bundle-analyzer)
489- [size-limit](https://github.com/ai/size-limit) - Size limit enforcement