JavaScript tree shaking, like a pro

This is a translation of an article about optimizing and reducing the size of an application bundle. It is good because best practices are described here, tips that you should follow to make trishhing work and throw unused code from the assembly. It will be useful to many, because now everyone uses assembly systems in which there is trishashing out of the box. But for it to work correctly, you must adhere to the principles described below.

image

Trichashing becomes the main technique when you need to reduce the size of the bundle and improve application performance on JS.

How trichashing works:

  1. You declare imports and exports in each module.
  2. The collector (Webpack, Rollup, or another) analyzes the dependency tree during assembly.
  3. Unused code is excluded from the final bundle.

image
The utility file exports two functions,

image
but only initializeName is used, formatName can be deleted.

Unfortunately, for the correct operation of trishaking, one setup of the collector is not enough. To achieve a better result, it is necessary to take into account many details, as well as make sure that the modules were not skipped during optimization.

Where to begin?


There are a huge number of guidelines for setting up trishaking. It's better to start diving into the topic with the official Webpack documentation .

It is worth mentioning that a few years ago I created a boilerplate with an already configured assembly and trichashing. So if you need a starting point for a project, my repository can be a good example of krakenjs / grumbler .

This article discusses working with Webpack, Babel, and Terser. However, most of the principles presented will work whether you use Webpack, Rollup, or something else.

Use ES6 syntax for imports and exports


Using ES6 imports and exports is the first and most important step to working trichashing.

Most other implementations of the “module” pattern, including commonjs and require.js, are non-deterministic during the build process. This feature does not allow collectors like Webpack to determine exactly what is imported, what is exported, and, as a result, what code can be safely deleted.

image
Options that are possible when using commonjs.

When using ES6 modules, import and export options are more limited:

  • You can import and export only at the module level, and not inside the function;
  • the module name can only be a static string, not a variable;
  • everything that you import must be exported somewhere.

image
ES6 modules have simpler semantics and rules of use.

Simplified rules allow assemblers to understand exactly what was imported and exported, and, as a result, determine which code is not used at all.

Do not allow Babel to transports imports and exports


The first problem you may encounter when using Babel to translate code is the default conversion of ES6 modules to commonjs. This prevents the collector from optimizing the code and throwing too much.

Fortunately, in the Babel config there is an easy way to disable module transpilation .

After you do this, the collector will be able to take on the transpilation of imports and exports.

Make your exports atomic


Webpack typically leaves exports intact in the following cases:

  • ;
  • ;
  • .

Such exports will either be fully included in the bundle, or completely removed. So, in the end, you may end up with a bundle containing code that will never be used.

image
Both functions will be included in the bundle, even if only one is used.

image
And here the class will be entirely added to the assembly.

Try to keep your exports as small and simple as possible.

image
Only the function that will be used will get into the final bundle.

Following this advice allows the collector to throw out more code due to the fact that now during the assembly process you can track which function was imported and used and which was not.

This tip also helps to write code in a more functional and reusable style, and to avoid using classes where this is not justified.

If you are interested in functional programming, check out this article .

Avoid side effects at module level


When writing modules, many people miss an important, but very insidious factor - the influence of side effects.

image
Webpack does not understand what window.memoize does, and therefore cannot throw this function.

Note the example above window.memoizewill be called at the time of import of the module.

As Webpack sees it:

  • ok, a pure add function is created and exported here - maybe I can remove it if it will not be used later;
  • then window.memoize is called, to which add is passed;
  • I don’t know what she’s doing window.memoize, but I know that she will probably call add and create a side effect.
  • so for safety I’ll leave the add function in the bundle, even if no one else uses it .

In reality, we are sure that it window.memoizeis a pure function that does not create any side effects and calls add if someone uses it memoizedAdd.

But Webpack does not know this and, for the sake of peace, adds the add function to the final bundle.

To be honest: the latest versions of Webpack and Terser are unusually good at detecting side effects.

image
We give Webpack more information and get an optimized bundle.

Now the collector has enough information for analysis:

  • called here memoizeat the module level, this can be fraught with problems;
  • but the function memoizecame from ES6 import, you need to look at the function in util.js;
  • indeed, memoize looks like a pure function, there are no side effects;
  • if no one uses the function add, we can safely exclude it from the final bundle.

When Webpack does not receive enough information to make a decision, it will take the safe path and leave the function.

Use tools to identify potential trishaking issues


I found two tools to identify problems.

The first tool is module concatenation , a plugin for Webpack, which allows you to achieve a significant increase in performance. It has a debugging option. It is worth noting that the factors preventing concatenation and trichashing are the same: for example, side effects at the module level. Take the warnings of the plugin seriously, as any problem potentially increases the size of the bundle.

The second is the plugin for the linter https://www.npmjs.com/package/eslint-plugin-tree-shaking . I have not integrated it into my boilerplate yet, because it did not support flow when I experimented with it. However, he pretty well identified problems with trichashing.

Be careful with libraries


Try to use libraries optimized for trichashing. If you import a large bundle of minimized code, for example jquery.min.js, there is a possibility that this module will not be optimized. It is better to look for a module from which atomic functions can be imported, and for assembly and minification use Webpack or Rollup.

Sometimes you can import an entire library. For example, when using the React production build you do not need to throw anything away - everything that is in it is already optimized.

If you use a library that exports individual functions, for example, lodash, try importing only the necessary functions and make sure that the others are excluded from the final bundle.

Use build flags


The DefinePlugin plugin for Webpack has a wonderful, but not the most famous feature - the ability to influence what code will be excluded during the build process.

image

If we pass it __PRODUCTION__: trueto the plugin, not only the function call validateOptions, but also its definition will be excluded from the final bundle .
This simplifies the creation of different bundles for development and production, and also helps to ensure that the code intended for debugging does not get into production.

Start assembly


It’s very difficult by eye to determine how Webpack will optimize a particular module.

So run the build, check the final bundle and see what happens. Take a look at the JavaScript code and make sure that there is nothing left in it that should have been thrown out by trichashing.

Something else?


If you know other useful tips, please write about this in the comments.

All Articles