JavaScript Bundle Optimization — Code Splitting and Tree Shaking
In this tutorial, you will learn how to reduce JavaScript bundle sizes through Code Splitting, Tree Shaking, and dynamic import patterns. JavaScript is the most expensive resource on the web, often blocking the main thread for hundreds of milliseconds. Doda Browser extensions use aggressive Code Splitting to keep initial bundle sizes under 50KB.
What You Will Learn
- How Tree Shaking eliminates unused exports from your bundles
- How to implement route-based and component-based Code Splitting
- How to analyze bundle composition with Webpack Bundle Analyzer
- How to set up dynamic imports for on-demand loading
Why It Matters
Every kilobyte of JavaScript costs approximately 5 milliseconds of CPU time on mobile devices. A 500KB bundle means 2.5 seconds of main thread blocking before the page becomes interactive. DodaTech reduced its main application bundle from 340KB to 85KB through systematic optimization.
Real-World Use Case
The DodaZIP web interface used a single monolithic bundle containing chart libraries, markdown parsers, and admin panel code. After splitting these into separate chunks loaded only on their respective pages, the initial bundle dropped to 45KB and Time to Interactive improved by 3 seconds.
Prerequisites
You should have experience with JavaScript ES6 module syntax and a module bundler like Webpack or Vite. Understanding of Babel Transpilation is helpful.
Step-by-Step Tutorial
Step 1: Analyze Your Current Bundle
Before optimizing, understand what is in your bundle. Use the Webpack Bundle Analyzer plugin.
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html'
})
]
};
Expected output: An interactive treemap visualization showing each module and its size. You will likely see large libraries like moment.js or lodash consuming hundreds of kilobytes.
Step 2: Enable Tree Shaking
Tree Shaking relies on ES6 module syntax (import/export). Ensure your Webpack configuration has mode: 'production' which enables tree shaking by default.
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: false // Only if your code has no side effects
}
};
// Before: importing the entire library
import _ from 'lodash';
_.debounce(myFunction, 300);
// After: importing only what you need
import debounce from 'lodash/debounce';
debounce(myFunction, 300);
Expected result: Unused lodash functions are removed from the bundle. The imported file drops from the full 71KB lodash to the 2KB debounce module.
Step 3: Avoid Barrel Files
Barrel files re-export from multiple modules and defeat Tree Shaking because bundlers cannot determine which exports are actually used.
// Bad: barrel file (index.js)
export { UserService } from './user.service';
export { AuthService } from './auth.service';
export { PaymentService } from './payment.service';
// Better: import directly from the source file
import { UserService } from './user.service';
Step 4: Implement Route-Based Code Splitting
Split your application at route boundaries so each page loads only its own code.
// React Router with lazy loading
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
Expected behavior: The browser downloads only the JavaScript for the current route. Navigating to Settings triggers a lazy chunk download of approximately 5-15KB instead of loading all pages upfront.
Step 5: Split Vendor Code
Separate third-party libraries from application code so vendor chunks can be cached independently.
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
Expected output: Two output files: vendors.abc123.js containing React and other libraries (approximately 120KB) and main.xyz456.js containing application code (approximately 30KB).
Step 6: Use Dynamic Imports for Heavy Features
Load heavy features like charts or PDF generation only when the user triggers them.
// Dynamic import for chart library
async function renderChart(data) {
const { Chart } = await import('chart.js');
const canvas = document.getElementById('myChart');
new Chart(canvas, { type: 'bar', data });
}
// Button click triggers the import
document.getElementById('show-chart').addEventListener('click', () => {
renderChart(salesData);
});
Expected behavior: Clicking the button triggers a network request for the chart.js chunk (approximately 60KB). The chart renders once the download and initialization complete.
Step 7: Configure Vite for Even Faster Bundling
Vite uses native ES modules and esbuild for extremely fast builds with built-in Code Splitting.
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash-es', 'date-fns']
}
}
}
}
};
Expected output: Vite produces small, focused chunks that load in parallel, reducing the total waterfall depth compared to a single large bundle.
Learning Path
flowchart LR A[Image Optimization] --> B[JavaScript Bundle Optimization] B --> C[CSS Performance] B --> D[Preload, Prefetch, Preconnect] C --> E[Critical Rendering Path] D --> E style B fill:#4f46e5,color:#fff style A fill:#6366f1,color:#fff style C fill:#6366f1,color:#fff
Common Errors
Not analyzing the bundle before optimizing: Developers guess which libraries are large instead of checking the analyzer. The result is effort spent on small modules while a 200KB library goes unnoticed.
Tree Shaking only works with ES6 modules: CommonJS modules (require()) cannot be tree-shaken. Use ES6 import syntax and configure Webpack to prefer ES6 module versions via the
modulefield in package.json.Creating too many chunks: Splitting every small module into its own chunk creates dozens of tiny HTTP requests. Aim for chunks between 20KB and 100KB each.
Forgetting to lazy-load heavy third-party libraries: Libraries like moment.js (231KB), chart.js (60KB), and pdfmake (300KB+) should always be lazy-loaded unless they are needed on every page.
No cache-busting on vendor chunks: Without a content hash in the filename, the vendor chunk never gets updated in the browser cache when you upgrade a library.
Splitting code that loads in the critical path: Splitting code that is already needed for the initial render adds an extra network round trip. Only split code that is conditionally loaded.
Practice Questions
- What is Tree Shaking and how does it work?
- How does the
import()function differ from a staticimport? - What is a barrel file and why is it bad for Tree Shaking?
- What does the splitChunks configuration do in Webpack?
- Why should vendor code be in a separate chunk?
Answers: 1. Tree Shaking statically analyzes ES6 module exports and removes unused exports from the production bundle. 2. import() returns a promise and loads the module asynchronously, enabling Code Splitting. Static import is evaluated at parse time. 3. A barrel file re-exports from multiple modules; bundlers cannot determine which exports are unused and include all of them. 4. It extracts shared dependencies into separate chunks to avoid duplication across entry points. 5. Vendor code changes infrequently, so a separate vendor chunk with a long cache lifetime means users download it only once.
Challenge
Take an existing React or Vue application, install Webpack Bundle Analyzer, and identify the three largest modules. Refactor the imports to be tree-shakeable, split the application by routes, and lazy-load the heaviest third-party library. Run the analysis again and document the size reduction.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro