Angular2: going production ready with Ahead-of-Time (AoT) compilation

Ahead-of-Time (AoT) compilation is a powerful tool. It speeds up application boot time by 2–3 times. However, it requires some tooling and refactoring. This story shares developer experiences from delivering an Angular2 application to production. The question is: have we stalled at 88 mph — or is it back to the future with AoT?

Ahead-of-Time (AoT) compilation is one of the features that has kept the Angular2 community on their toes for some time. It was first introduced in 2.0.0-rc.5 and actually took until 2.3.0 to be “ready-to-use”. The motivation behind AoT is improving web browser performance by moving compilation from runtime in the browser to build time.

Simply said, we, as developers, write an HTML template like this:

<button (click)="fetchData()">foo</button>

Angular, as a framework, needs to register a DOM event listener for the ‘click’ event and call the ‘fetchData()’ method of the Component class when the event happens. Before it can do so, Angular needs to parse the HTML template and make sense out of the syntax — that step of interpreting the template is performed by the Angular compiler and is moved to build time with AoT.

In this article, I will show a real-world example from delivering an Angular application for productive usage and share experiences with AoT. I will show load times of an application built with the traditional Just-in-Time (JiT) compiler, compared to the same application built with AoT compilation. Elsewhere, I’d like to thank the project team at my (former) employer and their customer who made it possible to write this story.

Test Subject

Before we actually start with the case study, let’s take a look at some numbers giving an indication of the application’s size.

The Angular application that is referred to in this article consists of 20 components with 20 HTML templates–of course–and 11 stylesheets. There are 4 directives and 2 pipes.

These numbers account for custom self-written code. Digging deeper, one would need to analyze binding expressions in HTML templates and count their number of occurrence, since AoT directly addresses these parts of the code.

On top of that, the application depends on the Ng2Bootstrap component library and the well-known Angular packages (common, compiler, core, forms, http, platform-browser / platform-browser-dynamic, router).

Both dependencies are included in the application’s bundle. Also, RxJS and Zone.js are included, but don’t add components, directives, or pipes to the bundle.

JiT — Baseline

Obviously, the first thing we did was looking at page loads in Chrome’s network panel and we made two observations from that.

Observation #1: it takes the application approx. 2 secs to load.

Please don’t look at this number as an absolute value but rather as an indication of how this is perceived by a human. While the browser bar is still spinning, she/he sees a blank page.

In this application, a CSS gradient, applied to the body by a global stylesheet, shows up at first glance. Because of that, you can sense the delay — imagine counting “twenty-one, twenty-two“ — until header image, login form, and navigation links show up. Since these UI blocks are implemented as Angular components they won’t show before the Angular application has finished bootstrapping.

Chrome network panel captured for an Angular application using the Just-in-Time compiler

Observation #2: we noticed a gap, a huge delay in the timeline.

The delay occurs between network fetches initiated by “index.html” (green border) and network fetches initiated by JavaScript files of the application (orange border). So we asked ourselves: are we stuck in a time warp?

To analyze that further, we measured the bootstrap time of the application.

We refer to bootstrap time as the timespan elapsed from script execution of the first line in the main.ts file to the constructor function of the top-level AppComponentbeing called. We measured the execution time in Chrome’s Timeline panel. Our build set-up is based on the popular Angular2 Webpack Starter by AngularClass.

In JiT mode, bootstrap time was approx. 1,600 msec. On top of that, the timespan fluctuated between ~1,300 msec and ~2,000 msec. Again, please don’t treat these as absolute values but rather as an indication. Notice that the bootstrap time varies almost one-third around the average time.

AoT — Treatment

Since we needed to improve page loads, we decided to incorporate the Ahead-of-Time compiler into our toolchain.

With the AoT compiler, HTML templates are validated at build time and additional errors are reported that you don’t see when running the JiT compiler. So, as good developers, we needed to refactor some of our code. The most common mistakes we made were:

  • Bound properties and callbacks were declared non-public. Since the generated factory classes access properties (and callbacks) that are bound in the HTML template, these need to be public in the component’s TypeScript class. To object-oriented programmers familiar with encapsulation and information hiding it felt natural to declare members and methods private unless they weren’t intended to be used as @Input() or @Output(). In our code, we had a colourful mix of public properties, private properties, and public by default properties; surplus, those were either bound or unbound. Well, we refactored.
  • Template expressions didn’t evaluate to a boolean type. When the AoT compiler encounters the *ngIfdirective, it will verify whether the expression is a boolean type. In some cases, we had expressions like *ngIf=”’foo' && 123". Such an expression evaluates to a number type in TypeScript (or string if operands are inverted). An award-winning joke is that the expression !’foo' && 123 evaluates to a number type, whereas inverting the expression to 123 && !'foo' evaluates to a boolean type. Yes, it makes perfectly sense from a compiler’s point of view. And yes, it made refactoring an entertainment show.
  • Event bindings caused “Supplied parameters do not match any signature of call target”. This usually happened when we declared a method with onEventBinding()as in the class and bound it in the template with onEventBinding($event). It means that binding parameters are not declared by a method. It can also happen when a type inferred from the template, e.g. onEventBinding('string'), doesn’t match the type declared in the class, e.g. onEventBinding(foo: number){/*..*/}. You should avoid such things, so we refactored.
  • Forms API: access properties by key vs. method access with get(), hasError(). Accessingform.controls.someName throws a type error in AoT. You need to use form.get('someName'). The same applies for control.errors.someName and control.hasError('someName'). To get rid of type errors, one needs to use the latter expressions. Easy going here, we refactored.
  • Factory providers must be exported, named functions. To functional programmers lambdas are such a nice thing to have, that we had quite a few of them in factory providers. It just felt obvious to write useFactory: () => {/*..*/}, just you should not. Again, we refactored.
Don’t worry. As long as you hit that compiler with the refactored code at precisely 88 miles per hour…

After all that refactoring, we needed to update webpack configuration, add npm scripts and an extra tsconfig.json file producing modules in “es2015” format. Then, we made the time jump back to the future and looked at a network panel like this:

Chrome network panel captured for the same Angular application using Ahead-of-Time compilation

Page loads are now down to approx. 600 msec. To put that result into perspective: bundling the application with the AoT compiler makes the exact same application load 3 times faster.

To give you a better perception of the results, pretend to perceive this as a human being. With the JiT compiler, you were able to count a “twenty-one, twenty-two” before the login form showed up.

Now, with the AoT compiler, you are barely counting to “twenty-…” before the login forms shows up.

To be fair, we have to say that we’re trading that improvement in page load time for an extra layer of complexity — we need one more build step, one more tool in the toolchain, and one more configuration orchestrating all the architectural building blocks.

To the end developer of the application the switch from JiT to AoT is transparent except the code refactoring described above; let’s call that a semi-transparent change. To the end user of the application, however, the switch is fully transparent.

On top of improved page load, bootstrap time is down to approx. 90 msec in AoT mode and fluctuates from ~80 msec to ~110 msec which implies a volatility of roughly 20%. Compared to the JiT compiler, that is 16 to 18 times faster.

However, we still observed a gap in the network panel and decided to look deeper into that. In the timeline panel, one can see the bootstrap time in the Console lane. The DOMContentLoaded and Load events are marked with a red arrow and fire just before the bootstrap is complete. The gap occurs after network fetches of JavaScript/CSS assets have finished (yellow and violet rectangles in the left) and just before bootstrap time starts ticking.

Angular application with AoT: page load captured in Chrome timeline panel

The timespan in question is labelled with “time warp 2.0”. Looking deeper into the timeline, one can see that this delay is caused by script evaluation time of main.<..>.bundle.js.

Elsewhere, if you look closely at the network panels again, you’ll notice that the main JavaScript asset has gone up from ~31 kB in JiT mode to ~163 kB in AoT mode. At the same time, the vendor JavaScript was reduced from ~1.3 MB to ~1.1 MB. All files are uncompressed and minified.

The vendor file became smaller since the Angular compiler is not included in an AoT-compiled bundle. The main file became bigger because of the auto-generated factory classes produced by the AoT compiler.

While our results don’t quite confirm the absolute numbers given in Demystifying Ahead-Of-Time Compilation In Angular, the bundle size can further be improved by using Rollup for tree-shaking. At the moment, we don’t use any dedicated tool for tree-shaking except the built-in algorithms in Webpack 2.2.0.

Remember that bootstrap time starts ticking when executing the first line of main.<..>.ts, thus our educated guess is that reducing bundle size will also reduce and ease “time warp 2.0”.

Wait a minute, Google. Ah… Are you telling me that you built a time machine… out of Angular?

Back to the future — Conclusion

Angular’s Ahead-of-Time (AoT) compilation is a powerful tool, that improves page load by such multiples that speed-up is sensible by human beings.

In our case, an application bundled with the AoT compiler loads 3 times faster than the same application bundled with the JiT compiler. Our case also suggests that bootstrap time is 16 to 18 times faster, implied by JavaScript execution times of the Angular framework.

Because of these results, we conclude that AoT is worth it while it introduces extra complexity and likely requires developers to refactor code.

These impressive results are achieved by substituting runtime tasks of the Angular framework with static code generation at build time.

To Android developers, such motivations and concepts aren’t entirely new.

Since there aren’t HTML templates on Android, developers write layouts the XML way. And since Android doesn’t know the term Component, they call the equivalent of that an Activity or Fragment. Views declared in XML are injected into Java classes by adding annotations (as opposed to Decorators in TypeScript).

When people started using such concepts, annotations were resolved at run-time by frameworks such as RoboGuice. While it made writing code easier, it implied performance penalties.

That in turn resulted in a whole bunch of new libraries starting to use annotation processors. Annotation processors generate boilerplate code at build time. Libraries like Butterknife, Dagger, or Android Annotations became popular and, all of a sudden, we were flicking through dependency injection frameworks from Spring to Guice to Dagger to Dagger2 just to be hunted down by Tiger.

Why? The community was trying to keep up with tools and technologies. Haven’t we been here before?

So, let me conclude this story with one final quote:

All you have to do is drive the time vehicle directly toward that screen accelerating to 88 miles an hour.
– Wait a minute, Doc. If I drive straight towards the screen, I’m gonna crash into those Indians.
Marty, you’re not thinking fourth dimensionally. You’ll instantly be transported back into 1885, and those Indians won’t even be there.