# Web Performance Optimisation Techniques III - Tree Shaking

Tree shaking is a term borrowed from the concept of shaking a tree to remove dead leaves. **In the context of web development**, it refers to the process of **eliminating unused or "dead" code** from an application **during the build process.** This is particularly useful in JavaScript applications, where large libraries might be imported, but only a small portion of them is actually used.

The term was popularised by the JavaScript bundler [**Rollup**](https://rollupjs.org/faqs/#what-is-tree-shaking), but it has since been adopted by other bundlers like **Webpack**, **Parcel**, and **Vite**. In this guide, we’ll explore the importance of tree shaking, how it works under the hood, and best practices to maximise its effectiveness.

## **Why is Tree Shaking Important?**

* **Smaller Bundle Sizes**:
    
    One of the primary benefits of tree shaking is that it reduces the size of the final JavaScript bundle. Smaller bundles mean faster load times, which is crucial for user experience, especially on mobile devices or slow networks.
    
* **Improved Performance:**
    
    By removing unused code, applications become more efficient. Less code means less parsing, compiling, and executing, which leads to better runtime performance.
    
* **Dead Code Identification:**
    
    Highlights unused modules or dependencies during bundling, helping developers identify and remove dead code from source files. This reduces bloat and technical debt, ensuring a leaner, more maintainable codebase over time
    

## **The Mechanics of Tree shaking**

To understand how tree shaking works, it is essential to dive into the mechanics behind it. At its core, tree shaking relies on **static analysis**, a process where the bundler analyses code without executing it.

Let’s break this down.

### **The Role of Static Analysis**

Static analysis is the backbone of tree shaking. It allows the bundler to analyse code at build time and determine which parts are actually used. This is only possible with **static code**.

**Static vs. Dynamic Code**

* **Static Code**: Code that **can** be analysed at **build time** or **before the application runs**. (e.g., `import` and `export` statements in ES6 modules).
    
* **Dynamic Code**: Code that **cannot** be analysed **until runtime** (e.g.,`require` statements in CommonJS or dynamic imports).
    

Tree shaking works **best** with static code because the **bundler can make decisions about what to include or exclude before the application runs**.

## **How Tree Shaking Works**

Tree shaking is a multi-step process that occurs during the build phase of an application. To better understand how this process works, let’s walk through a simple example step by step.

**Consider the following two files:**

1. `math.js`
    

```javascript
// math.js
export const add = (a, b) => a + b;

export const subtract = (a, b) => a - b;

export const multiply = (a, b) => a * b;

export const divide = (a, b) => a / b;
```

2. `main.js`
    

```javascript
// main.js
import { add, multiply } from './math.js';

console.log(add(5, 3));       // Only `add` is used
console.log(multiply(2, 4));  // Only `multiply` is used
```

**In this example,**

* The `math.js` module exports four functions: `add`, `subtract`, `multiply`, and `divide`.
    
* However, in `main.js`, only the `add` and `multiply` functions are imported and used.
    
* The goal of tree shaking is to ensure that only these two functions are included in the final bundle, while `subtract` and `divide` are removed as unused code.
    

### **Step-by-Step Process of Tree Shaking**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1741482277141/9a21d4ca-96f3-44a7-a9e5-82870ede49b5.png align="center")

1. ### Static Analysis of Imports and Exports
    
    The bundler analyses all `import` statements in the code to identify which modules and functions are being used. It also examines the `export` statements to understand what is being exposed by each module.
    
    #### How Static Analysis Works
    
    Static analysis is typically performed using an **Abstract Syntax Tree (AST)**. An AST is a tree representation of the structure of the code, where each node corresponds to a construct in the source code (e.g., a function, variable, or expression).
    
    **For example, the AST for** `math.js` **might look like this:**
    
    ```json
    {
      "type": "Program",
      "body": [
        {
          "type": "ExportNamedDeclaration",
          "declaration": {
            "type": "FunctionDeclaration",
            "id": { "name": "add" },
            "params": [...],
            "body": [...]
          }
        },
        {
          "type": "ExportNamedDeclaration",
          "declaration": {
            "type": "FunctionDeclaration",
            "id": { "name": "subtract" },
            "params": [...],
            "body": [...]
          }
        },
        ...
      ]
    }
    ```
    
    **The bundler uses the AST to:**
    
    * Identify which functions are exported from `math.js` (`add`, `subtract`, `multiply`, and `divide`).
        
    * Determine which functions are imported and used in `main.js` (`add` and `multiply`).
        
    
    By analysing these statements, the bundler can determine which functions are used and which are not.
    
2. ### **Building a Dependency Graph**
    
    Once the bundler has analysed the imports and exports, it constructs a **dependency graph**. This graph represents the relationships between modules and functions in the application.
    
    * **Nodes**: Each module or function is a node in the graph.
        
    * **Edges**: The edges represent dependencies between modules (e.g., `main.js` depends on `math.js`).
        
    
    The dependency graph helps the bundler understand which parts of the code are reachable from the **entry point** of the application (e.g., `main.js`).
    
    For example, the dependency graph for the above code might look like this:
    
    ```plaintext
    main.js
      └── math.js
          ├── add
          └── multiply
    ```
    
    **In this graph:**
    
    * `main.js` is the entry point.
        
    * `math.js` is a dependency of `main.js`.
        
    * Only `add` and `multiply` are connected to the entry point (`main.js`), meaning they are used in the application.
        
    
    The `subtract` and `divide` functions are not connected to the entry point, so they are considered unreachable.
    
3. ### **Dead Code Elimination**
    
    After building the dependency graph, the bundler identifies and removes any code that is not reachable from the entry point. This process is known as **dead code elimination**.
    
    * **Reachable Code**: Code that is directly or indirectly referenced from the entry point is kept.
        
    * **Unreachable Code**: Code that is not referenced (e.g., unused functions or modules) is removed.
        
    
    In the example above, the `subtract` and `divide` functions are unreachable and will be eliminated.
    

## **Challenges with Tree Shaking**

While tree shaking is a powerful optimisation technique, it is not without its challenges:

1. ### **Dynamic Code**
    
    Because tree shaking relies on **static analysis**, it cannot determine which parts of dynamically loaded modules will be used. As a result, the bundler may include the entire module in the final bundle, even if only a small portion is needed.
    
    **Example: Dynamic Imports**
    
    ```javascript
    import('./math.js').then(math => {
      console.log(math.add(2, 3));
    });
    ```
    
    **In this case:**
    
    * The bundler cannot determine which functions from `math.js` will be used at runtime.
        
    * As a result, it may include the entire `math.js` module in the bundle, even if only the `add` function is needed.
        
    
    **Another Example: CommonJS Modules**
    
    CommonJS is a module system used in Node.js and older JavaScript projects. It uses `require` and `module.exports` to define and import modules. However, CommonJS is inherently **dynamic**, which makes tree shaking difficult or impossible.
    
    Let’s rewrite our earlier example to use commonJS:
    
    #### `math.js`
    
    ```javascript
    // math.js
    exports.add = (a, b) => a + b;
    exports.subtract = (a, b) => a - b;
    exports.multiply = (a, b) => a * b;
    exports.divide = (a, b) => a / b;
    ```
    
    #### `main.js`
    
    ```javascript
    // main.js
    const math = require('./math.js');
    
    console.log(math.add(2, 3));       // Only `add` is used
    console.log(math.multiply(2, 4));  // Only `multiply` is used
    ```
    
    **In this example:**
    
    * The `require` statement dynamically loads the `math.js` module.
        
    * The bundler cannot determine which functions (`add`, `subtract`, `multiply`, `divide`) will be used at runtime.
        
    * As a result, the entire `math.js` module is included in the bundle, even though only `add` and `multiply` are used.
        
    
2. ### **Side Effects**
    
    Code with side effects cannot be safely removed, even if it appears unused.
    
    **For example:**
    
    ```javascript
    // side-effect.js
    console.log('This is a side effect!');
    
    // main.js
    import './side-effect.js';
    ```
    
    Even though `side-effect.js` does not export anything, the `console.log` statement is a side effect, so it will be included in the bundle.
    

## Best Practices for Effective Tree Shaking

To get the most out of tree shaking, follow these best practices:

1. **Use ES6 Modules**
    
    * Whenever possible, use ES6 modules (`import`/`export`) instead of CommonJS (`require`/`module.exports`). ES6 modules have a static structure that enables effective tree shaking.
        
2. **Avoid Dynamic Imports**
    
    * Use static imports whenever possible. Reserve dynamic imports for cases where lazy loading is absolutely necessary.
        
3. **Minimise Side Effects**
    
    * Whenever possible, try to write pure functions as they are easier to tree shake because they don’t have side effects.
        
    * However, If your code has side effects, use the `sideEffects` property in `package.json` to indicate which files have side effects. This helps the bundler make better decisions about what to include or exclude.
        
        **Example:**
        
        ```json
        //package.json
        {
          "sideEffects": [
            "src/some-side-effectful-file.js",
            "*.css"
          ]
        }
        ```
        
    
    4. **Optimise Third-Party Libraries**
        
        * Look for libraries that support ES6 modules and have minimal side effects.
            
        * Also, use tools like Babel or Rollup to convert a third-party library that uses CommonJS to ES6 modules.
            

## Popular bundlers That Support Tree Shaking

Below is a table summarising the popular bundlers that support tree shaking:

| **Bundler** | **Key Features** |
| --- | --- |
| [**Webpack**](https://webpack.js.org/guides/tree-shaking/#root) | Requires `mode: 'production'` and `sideEffects` property in `package.json`. |
| [**Rollup**](https://rollupjs.org/faqs/#what-is-tree-shaking) | First bundler to introduce tree shaking. Automatically performs tree shaking with ES6 modules. |
| [**Parcel**](https://parceljs.org/blog/beta1/#tree-shaking) | Zero-configuration bundler with built-in tree shaking support. |
| [**Vite**](https://vite.dev/) | Uses **ESBuild** for fast bundling and **Rollup** for production builds. Supports tree shaking during development and production. |

## In Conclusion,

Tree shaking is a powerful technique for optimizing JavaScript applications. By eliminating unused code, it significantly reduces bundle sizes, improves performance, and enhances maintainability. While tree shaking is most effective with ES6 modules, it’s important to follow best practices and be aware of common pitfalls to get the most out of it.
