We use cookies to enhance your experience on the site
CodeWorlds

Code splitting and tree shaking

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.

What is code splitting?

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.

Problems solved by code splitting

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 needed

Types of code splitting

1. Route-based code splitting

The 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}

2. Component-based code splitting

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}

3. Dynamic imports

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);

4. Vendor code splitting

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};

What is tree shaking?

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.

How does tree shaking work?

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));

Requirements for tree shaking

  1. ES Modules (ESM) - tree shaking only works with ES6 modules:
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 imports - imports must be static:
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 - prefer named exports over default exports:
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};

Practical optimization examples

CSS library optimization

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';

JavaScript library optimization

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);

Tree shaking in React

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';

Bundler configuration

Webpack

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};

Vite

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});

Rollup

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};

Advanced techniques

Preloading and prefetching

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);

Bundle analysis

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};

Custom loading strategies

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}

Side effects and package.json

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}

Practical tips

1. Monitoring bundle size

1# Using bundlephobia to check library size
2npm install -g bundlephobia-cli
3bundlephobia lodash
4bundlephobia lodash-es

2. Webpack Bundle Analyzer

1npm install --save-dev webpack-bundle-analyzer
2npx webpack-bundle-analyzer dist/static/js/*.js

3. Lighthouse CI for monitoring

1# .github/workflows/lighthouse.yml
2- name: Lighthouse CI
3  run: |
4    npm install -g @lhci/cli
5    lhci autorun

Full optimization example

1// 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}

Benefits and results

Bundle size savings

  • Lodash: 70KB -> 2KB (single function)
  • Moment.js: 230KB -> 11KB (date-fns)
  • Material-UI: 1.2MB -> 200KB (only used components)
  • Bootstrap: 150KB -> 30KB (only needed modules)

Performance impact

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};

Summary

Code splitting and tree shaking are fundamental optimization techniques for modern web applications. They allow you to:

  1. Reduce initial bundle size - users download only the code needed for the first render
  2. Speed up loading time - smaller files = faster downloads
  3. Improve User Experience - the application runs faster and smoother
  4. Save data transfer - especially important on mobile devices
  5. Enable better caching - vendor libraries change rarely

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.

Go to CodeWorlds