What’s up with tree shaking?
Me when I found out how tree shaking really works
I’m willing to bet you have a lot of “dead” code in your webpack
bundles. I used to think unused code was magically excluded by webpack
but it turned out I was wrong. Oh the woe for slow websites around the world. Here is how it all started…
The Discovery #
I put a website into Page Speed Insights today and one of the opportunities to make my site faster was to reduce unused Javascript. It appeared that around half of my Javascript was unused. If this Javascript is unused, why is it being bundled into my application?
A few years ago I heard the term tree-shaking, which was the removal of unused code from a bundle. The big hype I inferred was that Webpack 4 would tree-shake while bundling, thus eliminating unused code and making bundles significantly smaller. Was this true? Not really, for two reasons:
- Packages have to explicitly specify if they can be shook and most don’t
webpack
can only tree shakeimport
/export
statements but notrequire
This means that every time you npm install
and include a new library, chances are high you’ll be bloating your bundle with dead code as well. The documented way you specify if your code can be shook by webpack
is by adding the following property to your package.json
:
{
...
"sideEffects": false,
}
Lodash specifies this in their package.json, for example, so when you import _.filter
, none of the other 100+ utility functions will be in your bundle. A big savings. However, many popular React libraries I used did not specify that they could be shook.
My personal story #
I use react-burger-menu
. It’s a popular project with 4.5k stars on Github. There are 10 varieties of the menu (slide, stack, bubble, push, etc) and you import only the variety you want. I like slide
so I imported it into my project:
import { slide as Menu } from 'react-burger-menu'
But because react-burger-menu
does not have a sideEffects
property in its package.json
, when webpack
runs it will bundle all 10 varieties into my project. Wow. In no company I ever worked for was this ever mentioned, even when we were doing “performance” weeks. (To think back to those large React app bundles we created that were probably filled with unused code. Oh the load time! Oh the parse time!)
So I forked the library and added the sideEffects
property to the package.json
, but when I installed my version the bundle size stayed the same. What happened? The entry point for the package is:
{
...
"main": "lib/BurgerMenu.js",
}
When I looked inside this file I found that the code was using require
instead of import
and thus webpack
could not tree shake. I went in and changed the require
‘s to import
’s and like magic my bundle size went down by 7% and the website worked same as before.
The before:
Object.defineProperty(exports, '__esModule', {
value: true
});
exports['default'] = {
slide: require('./menus/slide'),
stack: require('./menus/stack'),
// other exports omitted for brevity...
reveal: require('./menus/reveal')
};
module.exports = exports['default'];
and after:
export * as slide from './menus/slide';
export * as stack from './menus/stack';
// other exports omitted for brevity...
export * as reveal from './menus/reveal';
See the commit.
Why do we live in a world like this? #
It’s a complicated topic. Many libraries bundle their code for web browsers as well as Node.js. Older version of Node.js did not understand import
/export
syntax and not every project uses webpack
so it’s best to stick with require
’s for maximum compatibility. I’m sure a lot of developers also assume that dead code is magically excluded and fewer have read the official webpack tree-shaking documentation, which I highly recommend for a more thorough explanation.
The main takeaway is this: if you’re using create-react-app
or webpack
, make sure your plugins are not including unused code in your bundle.
If you have questions or want to share some victories, you can find me on Twitter