Thanks to Filipe Silva for reviewing this post and to Rob Wormald for providing substantial insight into this technology.
In a previous article, I demonstrated how the Angular Build Optimizer modifies emitted JavaScript code to enable more effective tree shaking. To illustrate this, I set up a scenario with two Angular Material modules that were never actually used. After applying the Build Optimizer, the CLI/webpack pipeline managed to cut the bundle size roughly in half thanks to improved tree shaking:
If you are curious about how such impressive reductions are achieved, the answers lie ahead.
Keep in mind that, at the time of writing, the Angular Build Optimizer is still in an experimental phase. Even so, the outcomes above are quite encouraging.
Tree Shaking and Side Effects
The CLI relies on webpack for building, and to enable tree shaking, webpack flags unused exports so they can be dropped safely. A typical setup also employs UglifyJS to strip out those flagged exports. Uglify takes a cautious approach—it won't delete any code that might be required at runtime. For example, if Uglify suspects that a piece of code could trigger side effects, it will leave that code intact. Consider the following (deliberately trivial) example to see this in action:
(function(exports){
exports.pi = 4; // Let's be generous!
})(...);
The problem arises when ES2015+/TypeScript classes are transpiled down to ES5. The resulting class declaration becomes imperative code, and tools like UglifyJS can't be certain that this code is free of side effects. A thorough discussion of this issue is available here on GitHub. As a result, such code remains in the bundle even if it is never used.
To verify this behavior firsthand, I used Jurgen Van de Moere's Yeoman generator for Angular libraries to create a simple npm-based Angular package and then referenced it from a CLI-based application.
The package's entry point exports an Angular module that includes an UnusedComponent—as the name suggests, the application never touches it. It also exports an UnusedClass. Additionally, the same file (as an ES Module) exports another UnusedClass along with a UsedClass.
export {UnusedClass} from './unused';
export {OtherUnusedClass, UsedClass} from './partly-used';
export {SampleComponent} from './sample.component';
export {UnusedComponent} from './unused.component';
@NgModule({
imports: [
CommonModule
],
declarations: [
SampleComponent,
UnusedComponent
],
exports: [
SampleComponent,
UnusedComponent
]
})
export class SampleModule {
}
When the application is built without the Angular Build Optimizer, none of the unused classes get tree shaken. One explanation is the earlier point: Uglify cannot guarantee that the transpiled classes are side-effect free.
Marking Pure Code Blocks
To address the issue above, the Angular Build Optimizer annotates transpiled class declarations that are side-effect free with a special /*@__PURE__*/ comment. UglifyJS, in turn, honors these comments and removes any code blocks that are not referenced.
With this strategy, unused classes can be eliminated—but unused components remain. The reason lies in Angular's module system, which the next section clarifies.
Removing Angular Decorators
Since the NgModule decorator takes an array listing its Components, Directives, Pipes, and Services, any import of the module creates a reference to all those items. This blocks tools from shaking off unused building blocks.
Fortunately, after AOT compilation, Angular's decorators serve no purpose, so the Build Optimizer strips them all away. This produces code that no longer references unnecessary items, making it possible to shake them off.
Static Members
Transpiled static members often introduce side effects that hinder tree shaking too. Take this example from the Optimizer's Readme on GitHub, which shows the transpiled version of a class with a static member:
// Taken from the Angular Build Optimizer repo
var Clazz = (function () { function Clazz() { } return Clazz; }());
Clazz.prop = 1;
To eliminate the side effect, the Angular Build Optimizer rewrites the source as follows:
var Clazz = (function () { function Clazz() { } Clazz.prop = 1; return Clazz; }());
Importing tslib
There is one more optimization in the optimizer's toolkit. Depending on the compiler settings, the TypeScript compiler inserts helper functions at the top of each emitted file. These few lines can add up to noticeable overhead when repeated across many files—consider RxJS, which ships one file per operator, each carrying these helpers. The good news: the Optimizer swaps them out for an import from the tslib package. While this doesn't improve tree shaking directly, it does help shrink the final bundles further.
Using the Build Optimizer
Thanks to the three transformations mentioned above, the CLI and webpack can successfully shake off both the unused classes and the unused component. You can test this yourself: grab the example from my GitHub repository and generate a production build with and without the Build Optimizer. For the latter, pass the --build-optimizer flag on the command line:
ng build --prod --build-optimizer
Ensure you have a recent CLI version. At the time of writing, I used version 1.3.0-rc.0, which was the first to ship with the Optimizer. After the build, compare the generated bundles: the one produced without the Optimizer still contains all the unused classes, while the other does not.



