Skip to main content
  1. Frontends/
  2. React Guides/

Stop Shipping Dead Code: Mastering Tree Shaking in React Applications

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

It’s 2026. Internet speeds have increased, but so has the complexity of the average web application. Your users might be on 5G, but they are also dealing with bloated JavaScript bundles that parse slowly on mid-range mobile devices. If your React application takes three seconds just to become interactive (TTI), you’ve already lost a significant chunk of your audience.

Bundle size isn’t just a vanity metric—it’s directly correlated to Core Web Vitals and, subsequently, your SEO ranking and conversion rates.

The most effective weapon in your optimization arsenal? Tree Shaking.

While the term gets thrown around casually in code reviews, true tree shaking requires more than just hoping your bundler “figures it out.” It requires architectural discipline and a solid understanding of how static analysis works. In this guide, we aren’t just going to define it; we’re going to implement it, audit it, and ensure your production build is as lean as possible.

Prerequisites & Environment
#

Before we start slicing up your codebase, ensure you have a modern environment set up. Tree shaking relies heavily on static analysis provided by ES Modules (ESM).

  • Node.js: v20.x or higher (LTS recommended).
  • React: v19.x (Standard in 2026).
  • Bundler: We will focus on Vite (Rollup under the hood) as it’s the current industry standard, though concepts apply to Rspack and Webpack.

Setting the Stage
#

We’ll assume a standard project structure. If you are starting fresh, initialize a standard Vite Typescript project:

npm create vite@latest optimization-demo -- --template react-ts
cd optimization-demo
npm install

To visualize our progress, we need an analysis tool. Don’t guess; measure.

npm install -D rollup-plugin-visualizer lodash

Note: We are intentionally installing the standard lodash package to demonstrate common pitfalls.

1. The Foundation: ES Modules (ESM)
#

Tree shaking is essentially dead code elimination. Imagine shaking a tree: the green, living leaves (used code) stay attached, while the brown, dead leaves (unused code) fall off.

However, this metaphor only works if the tree structure is static.

Historical formats like CommonJS (require/module.exports) are dynamic. A bundler cannot determine at build time what will be used if the import path is calculated at runtime. You must strictly use ES Modules (import/export).

The Code Pattern
#

Avoid CommonJS:

// ❌ BAD: Dynamic, hard to tree-shake
const ui = require('./components/ui');
if (condition) {
  const extra = require('./utils/extra');
}

Embrace ESM:

// ✅ GOOD: Static structure
import { Button } from './components/ui';
import { heavyCalc } from './utils/math';

export const MyComponent = () => {
    // Logic here
};

If you are using Babel, ensure you aren’t transpiling modules to CommonJS before the bundler sees them. Your .babelrc or babel.config.json should generally look like this:

{
  "presets": [
    ["@babel/preset-env", { "modules": false }],
    "@babel/preset-react"
  ]
}

2. Visualizing the Build Process
#

To understand why some code persists even when you think it shouldn’t, we need to look at how the bundler views your code.

flowchart TD A[Source Code Entry] --> B{Parse AST} B --> C[Identify Import/Exports] C --> D{Is it used?} D -- No --> E{Has Side Effects?} D -- Yes --> F[Include in Bundle] E -- Yes --> F E -- No --> G[Drop Code 🍂] style G fill:#ff9999,stroke:#333,stroke-width:2px style F fill:#99ff99,stroke:#333,stroke-width:2px

Figure 1: The decision tree a bundler like Rollup or Webpack follows.

The critical and often missed step is decision node E: “Has Side Effects?”.

3. The “Side Effects” Trap
#

A “side effect” occurs when a file does something other than exporting functions—like modifying a global variable, attaching an event listener to the window, or importing a global CSS file.

If a bundler sees import './styles.css', it can’t remove that import because it applies styles globally, even if no JavaScript variable is imported.

If you import a library like this:

import { Button } from 'huge-ui-library';

The bundler checks huge-ui-library. If that library’s files contain code that runs immediately upon import (side effects), the bundler must include everything to be safe, breaking tree shaking.

The Fix: package.json
#

You can explicitly tell the bundler that your package (or specific files) contains no side effects. This allows the bundler to safely drop unused exports.

Open your package.json and add:

{
  "name": "my-app",
  "version": "1.0.0",
  "sideEffects": false
}

If you do have files with side effects (like global CSS), use an array:

{
  "sideEffects": [
    "**/*.css",
    "**/src/polyfills.ts"
  ]
}

4. Barrel Files and The Library Problem
#

Barrel files (index.ts files that re-export everything) are convenient for developers but can be disastrous for bundles if configured incorrectly.

Let’s look at the classic “Lodash Problem.”

The Problematic Import
#

In src/App.tsx:

import React from 'react';
// ⚠️ DANGER: This often pulls in the ENTIRE lodash library
import { debounce } from 'lodash'; 

export default function App() {
  const handleClick = debounce(() => console.log('Clicked'), 300);
  return <button onClick={handleClick}>Click Me</button>;
}

Even though you only want debounce, standard Lodash is built in CommonJS and isn’t modular by default.

The Solution
#

There are two ways to fix this.

Option A: Direct Path Import (The Manual Way) This works but is tedious.

import debounce from 'lodash/debounce';

Option B: Use ES Module Variants (The Modern Way) Replace legacy libraries with their ES equivalents.

npm uninstall lodash
npm install lodash-es
npm install -D @types/lodash-es

Now, the named import works perfectly:

// ✅ SAFE: Bundler can now tree-shake this
import { debounce } from 'lodash-es'; 

Library Comparison Table
#

Choosing the right library is half the battle. Here is a comparison of heavy libraries and their tree-shakeable alternatives.

Legacy / Heavy Library Tree-Shakeable Alternative Why Switch?
Moment.js date-fns or dayjs Moment is OOP and mutable (huge bundle). Date-fns is functional and modular.
Lodash lodash-es or radash Lodash-es uses ESM syntax allowing unused functions to be dropped.
Material UI (Legacy) MUI v5+ Ensure you use “path imports” or configure Babel plugins if using older versions.
React Bootstrap Headless UI / Radix Headless libraries provide logic without heavy, pre-styled components.

5. Configuring Vite for Aggressive Optimization
#

While Vite does a great job out of the box, we can tune the Rollup options in vite.config.ts to ensure we are visualizing the output and maximizing compression.

Here is a production-ready configuration:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    // Generates a stats.html file to inspect your bundle
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
  build: {
    rollupOptions: {
      output: {
        // Manual chunks to separate vendor code from app code
        manualChunks: (id) => {
          if (id.includes('node_modules')) {
            return 'vendor';
          }
        },
      },
    },
    // Ensure minification is using 'esbuild' (faster) or 'terser' (slightly smaller)
    minify: 'esbuild', 
  },
});

Run your build:

npm run build

This will open a visual map of your bundle. Look for large blocks. If you see a massive block for a library where you only use one function, tree shaking has failed, and you need to investigate that specific import.

Common Pitfalls and Troubleshooting
#

1. The * Import
#

Avoid importing everything as a namespace if you can help it.

// ⚠️ Potential Risk
import * as utils from './utils';

While modern bundlers are getting better at flattening namespaces, named imports are always safer for guaranteeing unused code removal.

2. Class Methods
#

Classes are notoriously hard to tree-shake. If you have a class with 50 methods and you only use one, the minifier often cannot be sure that the other 49 aren’t accessed dynamically via strings or internal calls.

Tip: Prefer functional programming and standalone utility functions over large “Helper” classes.

3. CSS-in-JS Overhead
#

If you use libraries like styled-components or emotion, be aware that the runtime itself adds weight. The styles themselves aren’t “tree-shaken” in the traditional sense. Consider zero-runtime solutions like Tailwind CSS or Vanilla Extract for the leanest possible bundles.

Conclusion
#

Tree shaking isn’t a magic toggle; it’s a byproduct of writing modern, clean, modular JavaScript. By sticking to ES Modules, properly flagging side effects in package.json, and regularly auditing your bundle with visualization tools, you can drastically reduce the footprint of your React applications.

Key Takeaways:

  1. Strictly use ESM: No require().
  2. Declare Side Effects: Add "sideEffects": false to your package.json.
  3. Audit Dependencies: Swap monolithic libraries for modular alternatives (e.g., date-fns over moment).
  4. Visualize: Make the build analysis part of your CI/CD pipeline.

Don’t let dead code haunt your users. Shake the tree, and keep your production builds pristine.


Further Reading
#