In the world of modern web development, where applications are becoming increasingly complex, managing the size of JavaScript bundles is crucial for performance. Two fundamental optimization techniques - code splitting and tree shaking - allow us to drastically reduce the size of code delivered to users.
Code splitting is a technique of dividing code into smaller fragments (chunks) that can be loaded on demand (lazy loading) or in parallel. Instead of sending all the application code in one large bundle, we can divide it into logical parts and load only what we currently need.
1// Problem: One huge bundle
2// main.bundle.js (2.5MB)
3// - Homepage code (200KB)
4// - Admin panel code (800KB)
5// - Online store code (1.5MB)
6// A user viewing only the homepage unnecessarily downloads 2.3MB of code!
7
8// Solution: Code splitting
9// main.bundle.js (200KB) - only homepage code
10// admin.chunk.js (800KB) - loaded only when needed
11// shop.chunk.js (1.5MB) - loaded only when neededThe simplest and most commonly used type - splitting code by application routes:
1// React Router with lazy loading
2import { lazy, Suspense } from 'react';
3import { Routes, Route } from 'react-router-dom';
4
5// Each component is a separate chunk
6const HomePage = lazy(() => import('./pages/HomePage'));
7const ProductPage = lazy(() => import('./pages/ProductPage'));
8const AdminPanel = lazy(() => import('./pages/AdminPanel'));
9
10function App() {
11 return (
12 <Suspense fallback={<div>Loading...</div>}>
13 <Routes>
14 <Route path="/" element={<HomePage />} />
15 <Route path="/product/:id" element={<ProductPage />} />
16 <Route path="/admin" element={<AdminPanel />} />
17 </Routes>
18 </Suspense>
19 );
20}Splitting code at the component level:
1// Lazy loading individual components
2const ExpensiveChart = lazy(() => import('./components/ExpensiveChart'));
3const HeavyDataTable = lazy(() => import('./components/HeavyDataTable'));
4
5function Dashboard({ showChart, showTable }) {
6 return (
7 <div>
8 <h1>Main panel</h1>
9
10 {showChart && (
11 <Suspense fallback={<div>Loading chart...</div>}>
12 <ExpensiveChart />
13 </Suspense>
14 )}
15
16 {showTable && (
17 <Suspense fallback={<div>Loading table...</div>}>
18 <HeavyDataTable />
19 </Suspense>
20 )}
21 </div>
22 );
23}Importing code in response to user actions:
1// Loading a library only when needed
2async function showAdvancedEditor() {
3 // CodeMirror is loaded only when the user clicks the button
4 const { EditorView, basicSetup } = await import('codemirror');
5 const { javascript } = await import('@codemirror/lang-javascript');
6
7 const editor = new EditorView({
8 extensions: [basicSetup, javascript()],
9 parent: document.getElementById('editor-container')
10 });
11}
12
13// Event listener on button
14document.getElementById('show-editor').addEventListener('click', showAdvancedEditor);Separating external libraries from application code:
1// webpack.config.js
2module.exports = {
3 optimization: {
4 splitChunks: {
5 chunks: 'all',
6 cacheGroups: {
7 // External libraries in a separate chunk
8 vendor: {
9 test: /[\\/]node_modules[\\/]/,
10 name: 'vendors',
11 chunks: 'all',
12 },
13 // Common application code
14 common: {
15 name: 'common',
16 minChunks: 2,
17 chunks: 'all',
18 enforce: true
19 }
20 }
21 }
22 }
23};Tree shaking is the process of removing "dead code" - code that is not used in the application. The name comes from the metaphor of "shaking a tree", where only unnecessary leaves (unused code) fall off.
1// utils.js - we export many functions
2export function formatDate(date) {
3 return date.toLocaleDateString();
4}
5
6export function formatTime(date) {
7 return date.toLocaleTimeString();
8}
9
10export function formatCurrency(amount) {
11 return new Intl.NumberFormat('pl-PL', {
12 style: 'currency',
13 currency: 'PLN'
14 }).format(amount);
15}
16
17export function validateEmail(email) {
18 return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
19}
20
21// main.js - we import only what we need
22import { formatDate, formatCurrency } from './utils.js';
23
24// Tree shaking will remove formatTime and validateEmail from the final bundle
25console.log(formatDate(new Date()));
26console.log(formatCurrency(100));1// ✅ Works with tree shaking (ES modules)
2import { specificFunction } from './module.js';
3
4// ❌ Does not work with tree shaking (CommonJS)
5const { specificFunction } = require('./module.js');1// ✅ Static import - tree shaking works
2import { debounce } from 'lodash-es';
3
4// ❌ Dynamic import - tree shaking does not work
5const moduleName = 'lodash-es';
6import(moduleName).then(module => {
7 const { debounce } = module;
8});1// ✅ Named exports - better for tree shaking
2export const add = (a, b) => a + b;
3export const subtract = (a, b) => a - b;
4
5// ❌ Default export with object - worse for tree shaking
6export default {
7 add: (a, b) => a + b,
8 subtract: (a, b) => a - b
9};1// ❌ Importing the entire Bootstrap library
2import 'bootstrap/dist/css/bootstrap.css';
3
4// ✅ Importing only needed modules
5import 'bootstrap/scss/functions';
6import 'bootstrap/scss/variables';
7import 'bootstrap/scss/utilities/display';
8import 'bootstrap/scss/utilities/spacing';1// ❌ Importing the entire Lodash library (>70KB)
2import _ from 'lodash';
3const result = _.debounce(handleSearch, 300);
4
5// ✅ Importing only the needed function from lodash-es
6import { debounce } from 'lodash-es';
7const result = debounce(handleSearch, 300);
8
9// ✅ Or even better - single function
10import debounce from 'lodash-es/debounce';
11const result = debounce(handleSearch, 300);1// ❌ Importing the entire Material-UI library
2import * as MUI from '@mui/material';
3
4// ✅ Importing only needed components
5import { Button, TextField, Card } from '@mui/material';
6
7// ✅ Or individual imports
8import Button from '@mui/material/Button';
9import TextField from '@mui/material/TextField';
10import Card from '@mui/material/Card';1// webpack.config.js
2module.exports = {
3 mode: 'production', // Enables tree shaking automatically
4 optimization: {
5 usedExports: true, // Marks used exports
6 sideEffects: false, // No side effects - safe removal
7 splitChunks: {
8 chunks: 'all',
9 cacheGroups: {
10 vendor: {
11 test: /[\\\/]node_modules[\\\/]/,
12 name: 'vendors',
13 chunks: 'all',
14 }
15 }
16 }
17 }
18};1// vite.config.js
2import { defineConfig } from 'vite';
3
4export default defineConfig({
5 build: {
6 rollupOptions: {
7 output: {
8 manualChunks: {
9 vendor: ['react', 'react-dom'],
10 utils: ['lodash-es', 'date-fns']
11 }
12 }
13 }
14 }
15});1// rollup.config.js
2export default {
3 input: 'src/main.js',
4 output: {
5 dir: 'dist',
6 format: 'es',
7 manualChunks: (id) => {
8 if (id.includes('node_modules')) {
9 return 'vendor';
10 }
11 }
12 },
13 external: ['react', 'react-dom']
14};1// Preload - high priority, needed immediately
2const AdminPanel = lazy(() =>
3 import(/* webpackPreload: true */ './AdminPanel')
4);
5
6// Prefetch - low priority, likely needed later
7const UserProfile = lazy(() =>
8 import(/* webpackPrefetch: true */ './UserProfile')
9);1// Bundle analysis using webpack-bundle-analyzer
2const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
3
4module.exports = {
5 plugins: [
6 new BundleAnalyzerPlugin({
7 analyzerMode: 'static',
8 openAnalyzer: false,
9 generateStatsFile: true
10 })
11 ]
12};1// Loading strategy based on internet connection
2async function loadComponentBasedOnConnection() {
3 const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
4
5 if (connection && connection.effectiveType === '4g') {
6 // Fast connection - load full version
7 return import('./components/FullFeaturedComponent');
8 } else {
9 // Slow connection - load simplified version
10 return import('./components/LightweightComponent');
11 }
12}1{
2 "name": "my-library",
3 "version": "1.0.0",
4 "sideEffects": false,
5 "main": "dist/index.js",
6 "module": "dist/index.esm.js"
7}1// Marking specific files as having side effects
2{
3 "sideEffects": [
4 "src/polyfills.js",
5 "src/global-styles.css",
6 "**/*.css"
7 ]
8}1# Using bundlephobia to check library size
2npm install -g bundlephobia-cli
3bundlephobia lodash
4bundlephobia lodash-es1npm install --save-dev webpack-bundle-analyzer
2npx webpack-bundle-analyzer dist/static/js/*.js1# .github/workflows/lighthouse.yml
2- name: Lighthouse CI
3 run: |
4 npm install -g @lhci/cli
5 lhci autorun1// Before optimization
2import React from 'react';
3import * as MUI from '@mui/material';
4import _ from 'lodash';
5import moment from 'moment';
6import './styles.css';
7
8function App() {
9 const [users, setUsers] = React.useState([]);
10
11 const debouncedSearch = _.debounce((query) => {
12 // Search users
13 }, 300);
14
15 return (
16 <MUI.Container>
17 <MUI.TextField onChange={(e) => debouncedSearch(e.target.value)} />
18 {users.map(user => (
19 <MUI.Card key={user.id}>
20 <MUI.Typography>
21 {user.name} - {moment(user.createdAt).format('DD/MM/YYYY')}
22 </MUI.Typography>
23 </MUI.Card>
24 ))}
25 </MUI.Container>
26 );
27}1// After optimization
2import React, { useState, lazy, Suspense } from 'react';
3import { Container, TextField } from '@mui/material';
4import { debounce } from 'lodash-es';
5import { format } from 'date-fns';
6import { pl } from 'date-fns/locale';
7
8// Lazy loading for rarely used components
9const UserCard = lazy(() => import('./components/UserCard'));
10
11function App() {
12 const [users, setUsers] = useState([]);
13
14 const debouncedSearch = debounce((query) => {
15 // Search users
16 }, 300);
17
18 return (
19 <Container>
20 <TextField onChange={(e) => debouncedSearch(e.target.value)} />
21 <Suspense fallback={<div>Loading...</div>}>
22 {users.map(user => (
23 <UserCard
24 key={user.id}
25 name={user.name}
26 date={format(new Date(user.createdAt), 'dd/MM/yyyy', { locale: pl })}
27 />
28 ))}
29 </Suspense>
30 </Container>
31 );
32}1// Performance metrics
2const performanceMetrics = {
3 beforeOptimization: {
4 bundleSize: '2.5MB',
5 firstContentfulPaint: '3.2s',
6 largestContentfulPaint: '4.8s',
7 timeToInteractive: '5.2s'
8 },
9 afterOptimization: {
10 bundleSize: '450KB',
11 firstContentfulPaint: '1.1s',
12 largestContentfulPaint: '1.8s',
13 timeToInteractive: '2.1s'
14 }
15};Code splitting and tree shaking are fundamental optimization techniques for modern web applications. They allow you to:
The key to success is a systematic approach: regular bundle audits, library size monitoring, and conscious decisions about what dependencies we add to the project.