In this post I will discuss different ways to generate Angular application bundles. I will show examples using Rollup, Webpack and the Closure compiler (ADVANCED_OPTIMIZATION).

Rollup or Webpack is currently the safest choice when picking an application bundler. Both are great bundlers with lots of benefits.

The Closure compiler is still experimental wrt Angular, but the Angular team has made great progress. At the time of writing the latest release is not compatible with the Closure compiler. However, Alex Eagle has made a custom, Closure compatible, build of Angular and RxJs. I am using a fork of his project for my Closure compiler example.

Generating Builds

In the following section I will create builds of my sample application using Rollup, Webpack and the Closure compiler.

I don't want to put any of the frameworks at a disadvantage, so here are the configurations used:

Rollup/Webpack.

Closure Compiler Fork.

Let me know if there are potential optimizations missing.

Next I will do some basic comparisons of the generated bundles.

Size

Optimizing bundle sizes is key to performance. Not only does it impact download times on slow networks. It also impacts the time it takes the browser to parse the JavaScript after it receives it over the wire.

For the purposes of this sample I have created an application of medium complexity. The application consists of a dynamic ReactiveForm with validation, and a recursive Treeview.

Below is a screenshot of the application.

I have deployed each build on my server in case you want to compare them:

Rollup
Webpack
Closure Compiler

One of the optimization techniques I've will discuss is called Tree Shaking. Tree Shaking is the process of ensuring that unused code is not included in the bundle. In this case that means omitting any unused code from the Angular framework. For more details on Tree Shaking you may want to read one of my other articles.

I have summarized the bundle sizes in the table below:

Webpack105k
Rollup89.6k
Closure Compiler 48.4k

Webpack vs Rollup

When we look at the numbers we see that Webpack produced a slightly bigger bundle than Rollup. This difference can be explained by some extra overhead added by Webpack to each bundled module.

Webpack wraps every module in a new function scope as seen below

(function(module, __webpack_exports__, __webpack_require__) { // One of these wrappers is added per bundled module /***/ (function(module, __webpack_exports__, __webpack_require__) { //bundled code goes here } }());

Adding this function wrapper around each module will result in a slightly bigger bundle.

By contrast, Rollup puts everything in a single function scope like so:

Another difference is that Rollup supports Tree Shaking.

(function () { //bundled code goes here “as is” without wrappers }());

That said, in my sample the size difference is not noteworthy. In larger apps there could perhaps be an impact on performance though.

Nolan Dawson has done much more thorough analysis of this in his excellent The cost of small modules article.

Still, before we judge Webpack, it's important to point out that Webpack is much more feature rich than Rollup.

Webpack makes up for the slightly bigger bundle size with more flexibility. One example of this is code splitting which can be used for lazy loading.

Rollup does not support code splitting, so you are limited to a single file bundle. This might become an issue as your application grows beyond what is practical as a single file download. Webpack will let you split the bundle into multiple files and lazy load on demand.

Closure Compiler

At 48.4k, the Closure compiler produced the smallest bundle by far.

This is because the Closure compiler goes far beyond just bundling. Rollup does Tree Shaking of a module at the statement level. Examples of statements are classes and functions. Rollup will be able to Tree shake out any unused statements, but it won't be able to evaluate the internals of the statements.

One example of this is unused methods in classes. Any unused methods in included statements will be included in the final bundle. This can even have a compounding effect if these unused methods refer to other modules. In that case the final bundle will include modules that are only referred to by unused methods.

I have an article here that describes how impactful this can be on the bundle size.

The Closure compiler does not suffer from this limitation and will run a much deeper analysis of the code. Through aggressive rewrites, function flattening and inlining it can potentially remove a lot of code.

One specific example in Angular is decorators.

Decorators like @Component are not needed at runtime when doing AoT. However, the decorator code is still included in the transpiled JavaScript.

If we inspect a Rollup or Webpack bundle we still see code like this:

AppComponent = __decorate$1([ Component({ selector: 'app', template: "some template", }) ], AppComponent);

This is the original decorator code that is no longer needed. It is not removed by Rollup or Webpack though since it's embedded in included code.

The Closure compiler will however see this as unnecessary code and correctly omit it from the bundle.

Closure Compiler and Lazy Loading

What about code splitting?

It turns out that the Closure compiler also supports code splitting, but I have not seen a working sample of this in Angular yet.

I do however have an article here showing how to do code splitting with the Closure Compiler in general.

Is Closure Compiler Viable?

The Closure compiler is not a new thing. With these amazing results, why have most people never heard of it?

The reason is that the aggressive optimizations come at the cost of several assumptions about your code. Unfortunately most frameworks are incompatible with the Closure compiler.

Frameworks with a clear separation between JavaScript and html templates are at a big disadvantage when it comes to the Closure compiler.

Part of Closure compiler optimizations is to shorten property names in JavaScript code. Since the Closure compiler doesn't have access to html templates there will be runtime errors when the template tries to bind to a person.firstName that was renamed to m.f by the Closure compiler.

Another challenge is frameworks with a separate static framework file download. The framework file typically defines global functions used by application code. This contract breaks when Closure compiler renames the code references back to the framework code.

There are however ways to give the Closure compiler cues in the form of externs to tell it to leave certain parts of the code alone. This complicates things though. I have an article here with some common conventions in case you are interested.

Angular 1.x vs Angular 2+

All the challenges I mention above perfectly describe Angular 1.x.

Luckily Angular 2+ with AoT is much more compatible with the Closure compiler since the templates are converted to plain JavaScript. It does not have a static framework download either any more. This makes it more realistic to do Closure compilation. The key is that the closure compiler now sees the complete picture and all your code. Code is no longer hiding out in html that isn't reconciled until runtime.

I guess there is still work to be done, but I am hopeful that the Closure compiler will become a real option in Angular going forward.

Another framework with fairly decent Closure compiler compatibility is Svelte. I have an article here in case you are interested.