JavaScript Bundle Optimization: Code Splitting, Tree Shaking, and Lazy Loading

JavaScript remains the heaviest resource on the modern web. The median page ships over 450 KB of uncompressed JS, much of it unused on initial render. Optimizing your bundle isn't optional—it's the highest-leverage performance improvement most sites can make.
Tree Shaking: Dead Code Elimination
Tree shaking relies on ES module static structure. Export only what's consumed and import specifically:
// Bad: imports the entire library
import { debounce } from 'lodash';
// Good: imports only what you need
import debounce from 'lodash/debounce';
// Best: tree-shakeable ESM build
import { debounce } from 'lodash-es';
Configure your bundler to treat all packages as ESM where possible. In webpack, ensure sideEffects in your package.json is set correctly:
{
"sideEffects": false
}
Mark files with side effects (polyfills, global styles) explicitly so the bundler doesn't eliminate them:
{
"sideEffects": [
"./src/polyfills.js",
"**/*.css"
]
}
Code Splitting by Routes and Components
Split bundles at route boundaries. With React and Vite, dynamic imports are straightforward:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
For component-level splitting, lazy-load heavy components that are below the fold or triggered by interaction:
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const ImageGallery = lazy(() => import('./ImageGallery'));
Each split creates a separate chunk loaded only when needed. The trade-off is an extra network request—keep chunks above 20 KB and below 150 KB for optimal HTTP/2 multiplexing.
Dynamic Imports for Interaction-Driven Code
Patterns like modals, tooltips, and context menus should load their JavaScript lazily, triggered by the first user interaction:
button.addEventListener('click', async () => {
const { showModal } = await import('./modal.js');
showModal();
});
This ensures the main bundle stays lean while heavy UI appears instantly when requested. Use webpack's /* webpackChunkName: "modal" */ magic comment for predictable chunk naming.
Module Federation for Micro-Frontends
When multiple teams own different parts of the application, Module Federation lets them deploy independently while sharing a shell:
// Remote app exposes its pages
new ModuleFederationPlugin({
name: 'checkout',
exposes: {
'./Cart': './src/Cart.jsx',
'./Checkout': './src/Checkout.jsx',
},
shared: { react: { singleton: true } },
});
The shell app consumes remotes at runtime, loading only the federated modules the current route needs. This prevents monolithic bundles while enabling independent team deploys.
Bundle Analysis: Find the Bloat
Before optimizing, measure. Use webpack-bundle-analyzer or Vite's rollup-plugin-visualizer:
npm run build -- --stats
npx webpack-bundle-analyzer dist/stats.json
Look for oversized dependencies that can be replaced with lighter alternatives. Moment.js (231 KB) → date-fns (19 KB). Lodash (549 KB) → native methods + individual functions. Chart.js (250 KB) → lightweight charting with uPlot or billboard.js.
Real-World Impact
A SaaS dashboard we optimized reduced JS from 520 KB to 180 KB gzipped using these techniques. LCP dropped from 4.2 s to 1.8 s. Time to interactive fell by 60%. The key was route-level splitting plus aggressive tree shaking on a handful of heavy vendor libraries.
JavaScript optimization is a compounding investment. Every kilobyte removed from the main thread makes the next interaction faster.
Our custom UI/UX design team builds performance-optimized frontends with lean JavaScript bundles from day one.
Related Insights

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

Caching Strategies for Web Applications: Browser Cache, CDN, and Application Cache
A complete guide to web caching strategies including browser cache control, CDN configuration, service worker caching, application-level caching, and cache invalidation patterns.

Code Splitting and Lazy Loading in React: Performance Optimization Guide
A comprehensive guide to code splitting and lazy loading in React applications including React.lazy, Suspense, route-based splitting, and component-level chunking.