User blog:Damian Bielecki/Tree Shaking - Ad Engineering Journey

Tree Shaking — Ad Engineering Journey
Our journey with tree shaking and some nuisance we had to face.

Our setup
In Ad Engineering we work on AdEngine our library for managing ads, as well as on integrating that library with various sites (apps) we support. So, we have a library and multiple apps that we need to adjust for tree shaking to work.

For building library (AdEngine) we use Typescript with Rollup.

For building apps we use Typescript with Webpack.

Legacy Build
Before tree shaking we used to split AdEngine into separate modules to limit bundle size. That solution was hard to maintain and did not yield expected results. Every module was an autonomous unit and it usually meant including duplicates of code in each of them. Moreover, it was not clear how to split modules for the best efficiency as apps we support may need different parts of the same module.

Solution?
Tree shaking wasn’t obvious nor the only solution we had in mind. One thing that remained clear was that we needed to update our building process because the duplicate parts of code, in all fairness not that significant at the moment, were starting to become a burden. For some time we entertained a thought of using Lerna for managing submodules. After lengthly discussion we came to a conclusion that we don’t really need modules in a sense of multiple packages, we only want to eliminate unused parts of our library. Tree shaking came out as an obvious candidate.

What is tree shaking?
Tree shaking is a process of removing unnecessary/unused parts of the code from the final bundle. Those parts are usually referred to as “dead code”.

How it is done?
Most modern bundling tools like Webpack already have a built in support for tree shaking, but it doesn’t mean it will work without any preparations on the library part.

How to setup a Typescript to support Tree Shanking?
The important part is for tsconfig.json to contain: {  "compilerOptions": { "target": "es5", "module": "es2015", } } The least important of the two is target. We want our code to be compiled to es5 so this is what we choose. While it is not essential for tree shaking to work it will become important in a later section.

module is the essential setting. It needs to be targeted at at least es2015 This will make our output look somewhat like that: import { ... } from '...'; ... export { ... }; This makes it possible for Webpack to eliminate unused parts/exports of our code. The only thing left to do is to set the module field of a package.json to tell Webpack that we have build that supports tree shaking.

This, in theory, is everything we need to do to make our library tree shakeable.

Problems
It turned out that things are not as simple as we may want them to be…

Classes
The first problem came out of "target": "es5". es5 does not support classes. This means that code: class Context { } gets compiled to: var Context = /** @class */ (function {  function Context {  }  return Context; }); It turns out that Webpack doesn’t understand that/** @class */ means that this fragment of code is pure (does not contain side effects). To combat that we had to write our own little plugin that would translate /** @class */ to something that Webpack can understand which is /*@__PURE__*/: export function classToPure { return { renderChunk(code) { return { code: code.replace(/\/\*\* @class \*\//g, '/*@__PURE__*/'), map: null, };  },  }; } This solves our problem and results in compiled code looking like that: var Context = /*@__PURE__*/ (function {  function Context {  }  return Context; });

Instances
The other problem was with our codebase itself. It is common for our code to contain a lot of instance exports: export const context = new Context; This creates a similar problem. As those instantiations are all over the place almost nothing got tree shaken. To fix that we created yet another plugin to tell Webpack that this in fact is also “pure”. export function instanceToPure { return { transform(code) { return { code: code.replace(/^(export const .+ = )new/gm, (match) =>     match.replace('new', '/*@__PURE__*/ new'),     ), map: null, };  },  }; } This plugin searches for every exported instantiation and transforms it to something like: export const context = /*@__PURE__*/ new Context; Now this may seem dangerous at first, as we may “purify” something we don’t want to. This is a risk we accepted when writing this plugin and it allowed us to efficiently tree shake our code.

Summary
Today, I am happy to say that it has been over half a year without a one incident caused by tree shaking. We were able to reduce our bundle size by ranging from 25% to 45% (up to 250KB). What is more important we are free to write and structure our code in a way that makes logical sense without having to worry where to put certain parts to reduce bundle size.

What are your stories with tree shaking and reduction of bundle size. Did you have similar problems or different altogether? Share it with us, I would be interested to find out what we could do better.