In this post I will talk about how to apply Tree Shaking to JavaScript code.

Tree Shaking is an optimized way of creating application bundles. The idea is to create a bundle that only includes code that is directly used by the application. Unused modules will be excluded from the final bundle. As a result we may end up with a drastically smaller application bundle.

The typical use case is removing unused parts of third party libraries. An application may take a dependency on a large third party library, but only use 10% of it. Tree Shaking makes it possible to only include those 10% of the library in the final bundle.

One requirement for successful Tree Shaking is ES6 modules. We have to write our code using ES6 modules since import and export statements are easier for the Tree Shaker to statically analyze than other alternatives like commonjs. It's however not important to use other ES6 syntax like classes. My samples use classes, but the ES6 modules part is what's important as far as Tree Shaking is concerned.

Tree Shaking typically works at the statement level. The Tree Shaker will evaluate which statements are in use in a particular ES2015 module.

How does the Tree Shaker determine if a statement is needed?

The Tree Shaker can determine if code (or statement) is needed by following import statements in combination with usage of the imported symbol. If an item is not imported, it's by definition not in use. If an item is imported, but the symbol never used, it is flagged as unused and excluded from the bundle.

Sample Application

Here is a simple application that I will use for the purposes of my examples.

main.js
import {GreetingService} from './greeting-service'; import {PersonService} from './person-service'; import {CustomerService} from './customer-service'; class Main { greet() { let greetingService = new GreetingService(); return greetingService.sayHello(); } getPerson() { let personService = new PersonService(); return personService.getPerson(); } } document.getElementById('greet').addEventListener('click', function() { let main = new Main(); alert(main.greet()); });
customer-service.js
export class CustomerService { getCustomer() { return {firstName: 'Joe', lastName: 'Smith'}; } }
greeting-service.js
export class GreetingService { sayHello() { return 'Hello'; } sayHi() { return 'Hi'; } }
person-service.js
export class PersonService { getPerson() { return {firstName: 'Joe', lastName: 'Smith'}; } }

The application consists of a few simple services and a common application root where the other services are imported.

Baseline (No Tree Shaking)

If we were to create a simple bundle without any Tree Shaking we would get something like the following:

(function () { 'use strict'; class GreetingService { sayHello() { return 'Hello'; } sayHi() { return 'Hi'; } } class PersonService { getPerson() { return {firstName: 'Joe', lastName: 'Smith'}; } } class CustomerService { getCustomer() { return {firstName: 'Joe', lastName: 'Smith'}; } } class Main { greet() { let greetingService = new GreetingService(); return greetingService.sayHello(); } getPerson() { let personService = new PersonService(); return personService.getPerson(); } } document.getElementById('greet').addEventListener('click', function() { let main = new Main(); alert(main.greet()); }); }());

As you can see the bundle is basically a concatenated version of the original modules. All the original code is included, even the unused CustomerService.

Tree Shaking

In the next example I will add Tree Shaking to see how things improve.

My example uses Rollup, a popular Tree Shaking framework.

Rollup requires a little bit of configuration. I have included the config object below:

export default { entry: 'app/main.js', dest: 'dist/standard.js', format: 'iife', moduleName: 'greetingModule' }

After adding Tree Shaking we see some improvement in the size of the bundle.

(function () { 'use strict'; class GreetingService { sayHello() { return 'Hello'; } sayHi() { return 'Hi'; } } class PersonService { getPerson() { return {firstName: 'Joe', lastName: 'Smith'}; } } class Main { greet() { let greetingService = new GreetingService(); return greetingService.sayHello(); } getPerson() { let personService = new PersonService(); return personService.getPerson(); } } document.getElementById('greet').addEventListener('click', function() { let main = new Main(); alert(main.greet()); }); }());

As you can tell, we no longer see any trace of CustomerService. Rollup detected that it was unused and excluded it.

Closure Compiler

Tree Shaking worked great! We no longer have to send the unused CustomerService to the users.

However, if we go through the bundle with a fine tooth comb, we notice that there is still room for improvement.

One of the limitations of traditional Tree Shaking is that it can only “shake” at the statement level. In this example, "statement" level means at the class level.

Our example suffers from this limitation since we have unused methods included in the bundle. Specifically the getPerson method is included even though it's never used by our application. This has a compounding effect since getPerson is the only reason personService is included in the bundle. Due to the structure of the code, Rollup is not able to detect that personService is needlessly included since it's only referenced from an unused method.

Similarly, Rollup has no choice but to include GreetingService in its entirety. Rollup has no way of splitting out the only method (sayHello) that is actually used by the application.

Wouldn't it be nice it we could optimize this further by running deeper analysis?

This is where the Closure compiler comes into play. Closure compiler with the ADVANCED setting will run a much deeper analysis of the code and potentially remove much more code.

Closure compiler is available both as a Java application and a JavaScript plugin. In my example I am using the JavaScript plugin integrated with Rollup.

The config object changes slightly to include the plugin:

import closure from 'rollup-plugin-closure-compiler-js'; export default { entry: 'app/main.js', dest: 'dist/closure.js', format: 'iife', moduleName: 'greetingModule', plugins: [ closure({compilationLevel: 'ADVANCED'}) ] }

Closure takes a much more aggressive approach to optimization.

Traditional Tree Shaking will include or exclude whole statements based on usage, but can't partially include an exported statement. Closure will actually rewrite our code through aggressive function inlining and function flattening.

After running Closure the entire app is reduced to a single line:

document.getElementById("greet").addEventListener("click",function(){alert("Hello")});

At the end of the day, all this app is doing is wire up an event handler to a button and trigger an alert message. Closure was able to detect that and get rid of all the layers in between.

This code reduction is obviously pretty amazing, but there are a few caveats...

Due to the aggressive optimizations made by Closure, it makes certain assumptions about the code. These assumptions may break many existing applications, so Closure is not always a viable alternative.

At the very least there has to be a call to the root module function from somewhere in the application. Otherwise you may end up with an empty bundle since Closure removed all the code until there is no code left :-).

In my sample the call to main.greet in the event listener is enough to tell Closure that my code is actually in use.

I have written an article here with some advice on how to make code more compatible with the Closure compiler.

I have put my code for this out on Github if you are interested in checking it out.