//

Improve JavaScript Performance with Tree Shaking

Tree shaking is the process used by bundlers like Webpack for dead-code elimination in JavaScript.

Tree shaking is the process used by bundlers like Webpack for dead-code elimination in JavaScript. It relies on the static structure of ES2015 or ES6 module syntax, i.e. import and export.

We can follow the same concepts used by these bundlers in our code to optimise our apps and increase performance.

Why do we need tree shaking?

The tree-shaking process reduces the code bundle size by shaving off the unused components and libraries included in our bundle. If this process wasn’t available, then we will be shipping all the components and libraries included in our app, even though they are not being used. This results in increased download time, longer decompressing, parsing and even executing times.

Source: Google

This way, tree-shaking helps us to:

  1. Reduce transpiled code size
  2. Increased bundle download time
  3. Shorter bundle decompressing time
  4. Faster code parsing time
  5. And faster execution time

The older JavaScript module import syntax like AMD modules and CommonJS, used to import code dynamically, which make it hard to analyse code size at the time it was transpiled. But with the introduction of webpack 2, built-in support for ES2015 was introduced as well as unused module export detection.

// CommonJS: The entire package is imported
const lodash = require('lodash'); 70.7K (gzipped: 24.7k);


// ES2015(ES6): A specific dependency is imported with tree shaking
import toLower from "lodash/toLower"; 2.2K (gzipped: 942);

By using ES2015 moulde import syntax here, the tree-shaking technique is importing a dependency by importing what’s only required from the global package, reducing the size of the imported dependencies, instead of importing the whole global package.

Minimising sideEffects

The new webpack 4 release expands on this capability with a way to provide hints to the compiler by using the sideEffects property in the package.json file to denote which files in your project are "pure" and therefore safe to prune if unused.

sideEffects is much more effective since it allows to skip whole modules/files and the complete subtree.

Side effects happen when a function or expression modifies state outside its own context. Some examples of side effects include making a call to an API, manipulating the DOM, and writing to a database. In order to exclude such files or make webpack aware of the state of the files it’ll be transpiling, we can go ahead and configure this in either the package.json file or within the webpack.config.json file like so:

{
  "name": "webpack-tree-shaking-example",
  "version": "1.0.0",
  "sideEffects": false
}

Or alternatively, in webpack.config.json file to specify which files to exclude:

{
  "name": "Webpack Tree Shaking Example",
  "side-effects": ["./src/a-file-with-side-effect.js"]
}

This assumes that any file that is not included in the list, is free of side effects.

You can also add these files via module.rules as below:

module.rules: [
  {
    include: path.resolve("node_modules", "lodash"),
    sideEffects: false
  }
]

Prevent Babel from transpiling ES6 modules to CommonJS modules

If you're using babel-preset-env in your Babel config, it automatically transpile your ES6 modules into more widely compatible CommonJS modules (converting imports into require). This becomes problematic when we want to start tree shaking, as webpack won’t know what to prune from the bundle if we use it.

To fix this issue, we can tell babel-preset-env to ignore ES6 modules:

{
  "presets": [
    [
      "env",
      {
        "modules": false
      }
    ]
  ]
}

Now instead of Babel converting your code for wide compatibility, webpack can convert the code as well as tree-shake.

Import only modules you want

Just like in the example where we imported the toLower module from lodash, we can do the same for the moduels we write.

// import sort from "../utils/sort";

import { quickSort } from '../utils/sort';

// sort.quickSort();

quickSort();

Rather than importing the whole module, we have only imported the export we needed. This will collectively with other module imports, will significantly reduce your final bundle size, resulting in faster bundle download speeds, increasing the page load speed.

Minify the output

By using the importand export syntax, we managed to mark our "dead code" to be dropped, but we still need to drop it from the bundle. In order to do that, set the modeconfiguration option to production.

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production',
};

Summary

What we've learned is that in order to take advantage of tree shaking, you must...

  • Use ES2015 module syntax (i.e. import and export).
  • Ensure no compilers transform your ES2015 module syntax into CommonJS modules (this is the default behaviour of the popular Babel preset @babel/preset-env - see the documentation for more details).
  • Add a "sideEffects" property to your project's package.json file.
  • Use the production mode configuration option to enable various optimizations including minification and tree shaking.