Chapter 36: Bundle Optimization
Bundle size directly affects load time. Every kilobyte of JavaScript must be downloaded, parsed, and executed before users see your app. This chapter covers analysis, tree shaking, code splitting, and asset optimization.
Analyzing Your Bundle
pnpm add -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
// ... other plugins
visualizer({
filename: "dist/bundle-stats.html",
gzipSize: true,
brotliSize: true,
template: "treemap", // or "sunburst", "network"
}),
],
});
After pnpm build, open dist/bundle-stats.html to see what is in each chunk.
Tree Shaking
Vite (via Rollup) automatically removes unused exports. Help it work effectively:
// ✅ Named exports — tree-shakeable
export function formatDate(date: Date): string { /* ... */ }
export function formatCurrency(amount: number): string { /* ... */ }
// ❌ Default export of object — NOT tree-shakeable
export default {
formatDate: (date: Date) => { /* ... */ },
formatCurrency: (amount: number) => { /* ... */ },
};
Import Only What You Need
// ✅ Import specific functions — unused exports are tree-shaken
import { format } from "date-fns";
// ❌ Import everything — entire library included
import * as dateFns from "date-fns";
// ✅ Specific Lucide icons
import { Search, Settings, User } from "lucide-react";
// ❌ All icons (huge bundle)
import * as Icons from "lucide-react";
Dynamic Imports
Load code only when needed:
// Route-based splitting (automatic with TanStack Router)
// Each route is a separate chunk loaded on navigation
// Component-based splitting
const RichTextEditor = lazy(() => import("@/shared/components/rich-text-editor"));
const ChartLibrary = lazy(() => import("@/shared/components/chart"));
// Conditional feature loading
async function loadAnalytics() {
if (import.meta.env.VITE_ENABLE_ANALYTICS === "true") {
const { initAnalytics } = await import("@/shared/lib/analytics");
initAnalytics();
}
}
Vendor Chunk Strategy
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks: {
"react-vendor": ["react", "react-dom"],
"tanstack": ["@tanstack/react-router", "@tanstack/react-query", "@tanstack/react-table"],
"effect": ["effect", "@effect/schema"],
"ui": ["@radix-ui/react-dialog", "@radix-ui/react-dropdown-menu", "@radix-ui/react-select"],
},
},
},
},
Why: Vendor code changes infrequently. Separate chunks mean returning users download only the app code that changed — vendor chunks are served from browser cache.
Image and Asset Optimization
// Vite handles asset optimization automatically
// Images < 4KB are inlined as base64
// Larger images get content-hashed filenames for cache-busting
// Use modern image formats
<picture>
<source srcSet="/hero.avif" type="image/avif" />
<source srcSet="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" loading="lazy" />
</picture>
// Lazy load below-the-fold images
<img src={imageUrl} alt={alt} loading="lazy" decoding="async" />
Compression
Configure your web server to serve compressed assets:
# nginx.conf
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;
# Better: Brotli compression
brotli on;
brotli_types text/plain text/css application/json application/javascript;
Vite can also generate pre-compressed files:
pnpm add -D vite-plugin-compression
Bundle Size Budgets
Set a CI check to catch bundle size regressions:
# In your CI workflow
- name: Check bundle size
run: |
pnpm build
MAX_SIZE_KB=500
ACTUAL_SIZE=$(du -sk dist/assets/*.js | awk '{sum += $1} END {print sum}')
if [ "$ACTUAL_SIZE" -gt "$MAX_SIZE_KB" ]; then
echo "Bundle size $ACTUAL_SIZE KB exceeds limit of $MAX_SIZE_KB KB"
exit 1
fi
Summary
- ✅ Analyze with rollup-plugin-visualizer before optimizing
- ✅ Tree shaking works best with named exports and specific imports
- ✅ Dynamic imports for routes, heavy components, and conditional features
- ✅ Vendor chunking separates infrequently-changing library code
- ✅ Image optimization with modern formats and lazy loading
- ✅ Compression (gzip/brotli) for all text-based assets
- ✅ Bundle budgets in CI to catch regressions