Bundle Optimization
The average website ships over 500KB of JavaScript. On a 3G connection, that's 10+ seconds of parsing and execution before the page is interactive. Bundle optimization isn't premature optimization â it's table stakes.
Code Splitting
Code splitting breaks your bundle into chunks that load on demand instead of all upfront.
Route-Based Splitting
The highest-impact split â each route loads its own code:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}Component-Based Splitting
For heavy components that aren't always visible:
const MarkdownEditor = lazy(() => import('./components/MarkdownEditor'));
const ChartLibrary = lazy(() => import('./components/ChartDashboard'));
function PostEditor({ mode }: { mode: 'write' | 'preview' }) {
return (
<Suspense fallback={<EditorSkeleton />}>
{mode === 'write' ? <MarkdownEditor /> : <Preview />}
</Suspense>
);
}Prefetching Splits
Load chunks before the user needs them:
const Settings = lazy(() => import('./pages/Settings'));
function NavLink() {
const prefetch = () => import('./pages/Settings');
return (
<Link
to="/settings"
onMouseEnter={prefetch}
onFocus={prefetch}
>
Settings
</Link>
);
}On hover or focus, the chunk starts downloading. By the time the user clicks, it's likely already cached.
Tree Shaking
Tree shaking eliminates dead code â exports that are imported but never used. It requires ES modules (import/export), not CommonJS (require).
// math.js â ES module
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function complexMatrix(data) { /* 500 lines */ }
// app.js â only `add` is used
import { add } from './math';
// multiply and complexMatrix are tree-shaken outTree Shaking Pitfalls
Side effects prevent tree shaking:
// This import has a side effect â it modifies a global
import './polyfills'; // bundler can't remove this
// Mark packages as side-effect-free in package.json
{
"sideEffects": false
}
// Or specify which files have side effects
{
"sideEffects": ["*.css", "./src/polyfills.js"]
}Barrel files can defeat tree shaking:
// components/index.js â barrel file
export { Button } from './Button';
export { Modal } from './Modal';
export { DataTable } from './DataTable'; // 50KB
// If your bundler can't trace through the barrel, importing Button
// may pull in DataTable too. Direct imports are safer:
import { Button } from './components/Button';Lazy Loading Beyond Components
Dynamic Imports for Libraries
async function generatePDF(data: ReportData) {
const { jsPDF } = await import('jspdf');
const doc = new jsPDF();
doc.text(data.title, 10, 10);
doc.save('report.pdf');
}The PDF library (200KB+) only loads when the user actually exports. This keeps the initial bundle lean.
Intersection Observer for Below-the-Fold
function LazySection({ children }: { children: ReactNode }) {
const [visible, setVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setVisible(true); },
{ rootMargin: '200px' }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return <div ref={ref}>{visible ? children : <Placeholder />}</div>;
}Analysis Tools
You can't optimize what you can't measure:
webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/stats.jsonGenerates a treemap showing exactly what's in your bundle. Look for:
- Duplicate dependencies (two versions of lodash)
- Unexpectedly large packages
- Packages that should be lazy-loaded
import cost (VS Code extension)
Shows the gzipped size of imports inline. Catch heavy imports before they ship.
Lighthouse CI
# .lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"resource-summary:script:size": ["error", { "maxNumericValue": 200000 }],
"total-byte-weight": ["warn", { "maxNumericValue": 500000 }]
}
}
}
}Optimization Checklist
- Analyze first â run bundle analyzer before making changes
- Route-split â every route should be a separate chunk
- Audit dependencies â replace heavy libraries with lighter alternatives (
date-fnsovermoment,clsxoverclassnames) - Check barrel files â ensure they don't prevent tree shaking
- Lazy-load heavy features â charts, editors, PDF generators, maps
- Set budgets â fail CI when bundle size exceeds thresholds
- Monitor over time â bundle size creep is invisible without tracking
Interview Signal
Bundle optimization questions test engineering discipline:
- Measurement-first mindset â analyzing before optimizing, not guessing
- Trade-off awareness â more splits mean more network requests; there's an optimal granularity
- Tooling fluency â knowing how to configure splitting, analyze output, and set up CI budgets
- Impact prioritization â route-level splitting first, then heavy components, then micro-optimizations