Simply put, tree shaking means removing unreachable code (also known as dead code) from a package

“You can think of your app as a tree. The source code and libraries you actually use represent the green, living leaves of the tree. Dead code represents the dead brown leaves of trees consumed in the fall. To get rid of dead leaves, you have to shake the tree and make it fall.”

“You can imagine your application as a tree. The source code and libraries You actually use to represent The green, living leaves of the tree. Dead code represents the brown, dead leaves of the tree that are consumed by autumn. In order to get rid of the dead leaves, You have to shake the tree, causing them to fall.”

The term was first popularized by the Rollup team in the front-end community. But all authors of dynamic languages have been grappling with this problem for a long time. The idea of tree-shaking algorithms dates back at least to the early 1990s.

In the JavaScript world, tree-shaking has become possible since the ECMAScript Module (ESM) specification in ES2015 (formerly known as ES6). Since then, most packers have enabled tree shaking by default, as they reduce the output size without changing the behavior of the program.

ES Modules Vs CommonJS

CommonJS predates the ESM specification by several years. It is designed to address the lack of support for reusable modules in the JavaScript ecosystem. CommonJS has a require() function that takes external modules based on the provided path and adds them to the scope at run time.

Require is a function, and like other functions, it is difficult to evaluate the result of running at compile time (because it is a function). Most importantly, require can be called anywhere in the code: wrapped in another function call, in an if/else statement, in a switch statement, and so on.

By studying and reflecting extensively on the CommonJS architecture, the ESM specification defines this new architecture, in which modules use the respective keywords import and export. Therefore, there are no more function calls. The ESM is also only allowed as top-level declarations — it is impossible to nest them in any other structure because they are static: the ESM does not depend on runtime execution.

Scope And Side Effects

const pure = (a:number, b:number) => a + b

const impure = (c:number) => window.foo.number + c
Copy the code

Bundlers determine whether a module is pure by evaluating as much of the provided code as possible. But code at compile time or build time cannot be evaluated. Therefore, assume that packages with side effects cannot be properly eliminated, even if they are not accessed.

As a result, the bunder now accepts a key in the module package.json file, allowing developers to declare whether the module is side-effect free. This way, developers can opt out of code evaluation and prompting the package; Code in a particular package can be eliminated if there are no accessible imports or require linked to statements in that particular package. This not only makes packages leaner, but also speeds up compilation times.

{

"name": "my-package",

"sideEffects": false

}
Copy the code

So, if you’re a package developer, use sideEffects carefully before shipping, and, of course, modify it every time you ship to avoid any unexpected damaging changes.

In addition to the root sideEffects key, purity can be determined file-by-file by annotating inline comments in method calls with /* @__pure__ */.

Copy the code
const x = */@__PURE__*/eliminated_if_not_called()
Copy the code

Avoid Premature Transpiling

Unfortunately, one particular problem that is quite common and can have a devastating effect on tree shaking is premature transfer optimization. In short, this is what happens when you integrate different compilers into your wrapper using special loaders. Common combinations are TypeScript, Babel, and Webpack — and their various combinations with each other.

Both Babel and TypeScript have their own compilers, and their respective loaders allow developers to use them for easy integration. That’s the hidden problem.

These compilers arrive in your code before the code is optimized. Whether by default or misconfigured, these compilers typically output CommonJS modules instead of ESM. As mentioned in the previous section, CommonJS modules are dynamic and therefore cannot be evaluated correctly to eliminate dead code.

This is now becoming more common with the growth of “homogeneous” applications, that is, applications that run the same code on both the server side and the client side. Because Node.js does not yet have standard support for ESM, when compilers target the Node environment, they output CommonJS.

Therefore, be sure to check the code your optimization algorithm is receiving.

Tree-Shaking Checklist

Now that you know how bundling and tree-shaking work, let’s draw a list for yourself that you can print out in a convenient place when you revisit the current implementation and code base. Hopefully, this will save you time and allow you to optimize not only the perceived performance of your code, but even the build time of your pipeline!

  1. With the ESM, not only do you use it in your own code base, but you also support packages that output the ESM as its consumables.
  2. Make sure you know exactly which dependencies, if any, have not been declaredsideEffectsOr set them totrue.
  3. Use inline annotations to declare pure method calls when using packages with side effects.
  4. If you are exporting the CommonJS module, be sure to optimize your package before transforming the import and export statements.