Tree-Shaking a React Component Library in Rollup

Introduction

In this article, I will cover some of the pointers that I learnt about "Tree Shaking" the hard way, after countless trials and errors, Googling and what's not.

Today, I'm going to try my best explaining some core differences between "Treeshaking in a Library/App" and "Treeshaking a Library usage in an App", based on my understanding.

Here is the Code Repo that I'm working on for this study. Consider giving it a Star if you find it helpful haha.. It's also related to this article that I've written earlier on.

Okay people, let's get into it!

Topics Coverage

Please feel free to skip to the sections of your interest.

  1. Prerequisite to Treeshaking - ESM
  2. Treeshaking in Library (Rollup)
  3. Treeshaking Library usage in App - sideEffects

1. Prerequisite to Treeshaking - ESM

This is covered in many other articles already. And yes, I will emphasize just one more time here. It is absolutely important to have your code written in ES Module (ESM) format if you want to make it treeshake-able.

Reason being, with its static structure, i.e. "import" and "export", bundler will be able to determine what is used and unused during compile time to perform dead code elimination.

I highly recommend that you read up these 2 passages here to gain a deeper understanding of it - Webpack - Tree Shaking and ES6 - Static Module Structure

2. Treeshaking in Library (Rollup)

In Rollup, there is literally nothing you need to setup to gain the Tree Shaking capability within your codes. The only thing to do is just to make sure that you code in ESM, that's it!

During the build, Rollup will eliminate your unused codes automatically. This is described here. However, this only implies that the resulting bundle size of your library will be optimal, without all those unused codes. Period.

It does not guarantee that your Client App will be able to consume it optimally, i.e. only import what it needs from your library.

3. Treeshaking Library usage in App - sideEffects

Truth is, there is a property you need to set at package.json - sideEffects. It is not a standard field in package.json, i.e. it's up to individual bundler to define how it's used. At least, that's what I understood through my research. Webpack documented its usage here really well in comparison to Rollup.

This flag is super important apparently as it basically hints your bundler whether your codes are "pure" or "impure".

In my understanding, "impure" in this case means that some import statements in your code contains side-effects, i.e. content not being used directly after import but it contains logics (side-effects) executed from within that imported file itself.

Stop. Let it sink... Read the line above one more time.

In React, consider these following codes.

NastyLogics.ts

1const NastyLogic = () => console.log('Nasty!!!');
2
3window.ImportantLogic = console.log('Nasty!!!');
4window.TestLogic = console.log('Nasty!!!')
5
6export default NastyLogic;
7

App.tsx

1import React from 'react';
2import './App.css';
3import './NastyLogics';
4
5function App() {
6  return <div>Testing</div>;
7}
8
9export default App;
10

Though the './App.css' is imported and not used anywhere directly in this component, it might still contains some styling for the div element for instance. Thus, that App.css is considered impure here (if there is a style for div).

Same goes for './NastyLogics'. It defines 2 new functions in window object, which might be important for some other parts of the App. Thus, it is considered impure too. In reality, it is a bad practice but it can happen.

Configuration

The sideEffects property configuration is similar whether you are using Webpack or Rollup. It accepts either a boolean value (true/false) or a string array (specifying files that contain side-effects). The default value is 'true'.

  • true (default) - all files are expected to have side effects
  • false - all files are expected to not have side effects
  • [filename1, filename2...] - all files are expected to not have side effects except files specified. The name accepts glob patterns.

In my component library at this point in time, the configuration looks like the following. Since I am still relying on some CSS files, I marked CSS as a side effects here. Complete source is here.

1{
2
3  ...
4  "sideEffects": [
5    "**/*.css"
6  ],
7
8  ...
9}
10

There are 2 wonderful repos that shed some lights on this subject matter to me - Lodash (ES branch) and Material UI. Checkout their package.json there. They both set the sideEffects as false. That is the key reason why we are able to import specific module directly from their root index file and yet our App's bundler knows exactly to import only those specific module during build.

Bundle Size Analysis - Client App

I'm using a CRA app and a local built version of my library to test this. Here is the guide to setup bundle size analyzer for CRA.

Following is the code snippet that I've used for my test. I'm just trying to make sure that the import Button here, is truly just importing the Button instead of my entire library.

1import React from 'react';
2import { Button } from 'codefee-kit';
3
4function App() {
5  return <Button text="Button"></Button>;
6}
7
8export default App;
9

Following are the results for "Without sideEffects flag" and "With sideEffects flag" specified in my package.json file.

Without sideEffects flag (default=true)

Indeed, all my other components were bundled besides Button component. Definitely not our expected "Tree Shake" result.

default bundle analysis with sideEffects as true
https://github.com/DriLLFreAK100/bundle-analysis/blob/main/Without%20sideEffects%20flag.png

With sideEffects flag

After setting the sideEffects as false except CSS files, it works! The Client App now only bundles my Button and Typography (used in Button) components.

bundle analysis with sideEffects as false
https://github.com/DriLLFreAK100/bundle-analysis/blob/main/With%20sideEffects%20flag.png

Conclusion

To recap, there are 2 parts to this, i.e. Treeshaking in Library/App and Treeshaking Library usage in App. The former is just treeshaking within its own project, where else the latter is concerned with "treeshaking" and optimization as a 3rd party package consumption.

The former is easy to implement with modern bundlers. The latter has a gotcha and it's mostly something to do with the sideEffects flag in package.json. This flag is not a standard package.json field and it's totally up to the individual bundler to define its usage.

In that regards, Webpack did a splendid job documenting down the details. However, at Rollup, that is not really the case. It was not explicitly documented anywhere, which made the journey a rather frustrated one for me. In case anyone is looking for the relationship between sideEffects flag and Rollup, here is the only official comment I found on GitHub.

I am still trying my best, figuring out the bits and pieces around these. Hopefully, the texts in this article is accurate enough and enlightens some of you out there in the same boat as I was.

All in all, JavaScript bundling is still a very deep and complicated topic to understand and master. I shall continue to experiment and decipher these beasts when the situation calls for it again.

Speaking of which, there is another bundler that gained quite some traction recently, esbuild. It's a bundler written in Golang. Perhaps, it's time for another experiment? hahaha.. let's see...


Hoho... Look at these svelty coffee beans. It's some Red Catuai from Honduras, roasted by Nylon Coffee Roasters, Sg. Had lotsa fun brewing this one!

coffee beans from Nylon Coffee Roaster
Coffee beans from Nylon Coffee Roaster. Great stuffs! Check them out! https://nylon.coffee

Resources

  1. https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking
  2. https://developers.google.com/web/fundamentals/performance/optimizing-javascript/tree-shaking
  3. https://webpack.js.org/guides/tree-shaking/
  4. https://exploringjs.com/es6/ch_modules.html#static-module-structure
  5. https://stackoverflow.com/a/49203452/6096478
  6. https://github.com/lodash/lodash/blob/4.17.21-es/package.json
  7. https://github.com/mui-org/material-ui/blob/master/packages/mui-material/package.json
  8. https://github.com/rollup/rollup/issues/2593#issuecomment-616208761