Tree-shaking 101

Everything you need to know

I spent a significant amount of time at work these last few months exploring tree-shaking and finding that it wasn't nearly as easy as I thought. We have been trying to build a simple UI library and wanted to get it to tree-shake to prevent consumers from importing the whole thing when putting a button on the page. As a team, we thought this would be pretty simple, but it turned out we were wrong...

In this post, I want to share my findings to spare you the same pain I went through and hopefully save yourself some time too.

What is tree-shaking?

Simply put, tree-shaking is the act of analysing code at build time and removing anything that will not get executed. It is not unique to the JavaScript/TypeScript universe, but I will be coming at it from that angle, as it is what I have experience with. Interestingly, the concept has been around since the 1990s according to Wikipedia, when it was first used in LISP.

However, the term "tree-shaking" as we know it was coined by the team behind Rollup, a bundler that is renowned for its tree-shaking abilities. You can think of the codebase as a tree, and when it is "shaken" (bundled) the dead leaves (bits of code that will not get executed) fall off, making the codebase leaner.

Why tree-shake?

As with anything web-related - performance!

A smaller bundle means less data for your users to download and therefore (hopefully) your app will load faster.

Using techniques like tree-shaking means we can spend our time as developers working on many small files, enjoying our developer experience, without having to worry about how this all gets sent to the browser - that's the bundler's job.

When a bundler merges all these small files into one (or a few) large ones, it will get rid of spacing and comments and even convert some of the developer-friendly names to single-character ones, all to save those precious bytes from coming down the wire and hitting users where it hurts.

Tree-shaking just takes this one step further and removes any code the bundler thinks will not end up getting used in the final app.

Where to tree-shake

This may seem obvious to some of you, but it wasn't to me when I first started looking into all of this so I thought I'd mention it. If you're building a library for use in an app, the tree-shaking of said library should be done in the consuming app, by the bundler the app is using.

All you can do in the library itself is set it up in such a way that it can be tree-shaken in the first place, which involves a few things to think about - namely what type of JavaScript modules to use and what to do about side effects, which I'll discuss in the next few sections.

What type of modules do you need to be able to do it?

Something I found out pretty quickly when I started researching tree-shaking is that ES Modules are the only type of JavaScript modules that will allow proper tree-shaking.

This means that you will need a build tool that will output your code in ES2015+ format. What this means is that your code will use modules that are imported and exported using the following syntax:

Importing: import MyComponent from 'my-fancy-module';

Exporting: export const MyFavouriteComponent = () => { ... };

I explored many different build tools and found that Rollup, esbuild and SWC were the easiest to set up. I even tried just using the TypeScript compiler to generate ESM code which actually worked really well, it was just a bit slower than some of the other tools were.

Word of warning: I did not find Webpack easy to set up for this. Perhaps I was missing a trick, but whatever I tried, it just wanted to output its own custom format that did not resemble ESM. Webpack is better used

Side effects

Ideally, you should not have any side effects in your code. A side effect is a piece of code that has a special behaviour other than exposing one or more exports. Usually this special behaviour impacts the global scope so it's thing like setting attributes on window, using the global scope and amending JavaScript's inner workings (using polyfills or amending its prototypes).

The reason the bundler doesn't like side effects is because it has no way of knowing whether the code in the file is safe to exclude. If you have some code that sets attributes on window, leaving it out will probably cause your app not to function, so the bundler figures that it's better just to include it and all the files it references.

Where things can get a bit murkier is with third-party code. If you're including a library, this could have side effects without you even knowing about them. I would recommend avoiding third-party libraries as much as possible if you're building a tree-shakeable library to be honest, unless the authors have expressly said they have made their library tree-shakeable as well.

That's all there is to treeshaking really. I've already written an article on how to troubleshoot tree-shaking so if you're still stuck, do check that out. Next in this series will be an article on how you can tell whether your code is capable of being tree-shaken