Angular: Writing AoT-friendly applications

Some time ago, Angular went ahead-of-time … and left behind some developers.

This article describes development guidelines and instructions how to write AoT-compatible apps with the Angular CLI. It serves supplementary information to the official Ahead-of-Time Compilation cookbook. The article talks through this topic by an example app.


Motivation

Leaving people behind sounds very harsh. At the same time, it’s something very serious. Recently, some development teams — who I work with — started to upgrade from Angular v2 to v4 and switched from custom gulp tasks to the Angular CLI. And they migrated their code base to work with AoT (Ahead-of-Time) compilation.

Obviously, when re-writing large parts of your app’s code base, you are going to face some pain points. And you also gain some insights and earn experience points to level up. Interestingly, a few people out there are asking for better developer guidelines w/ AoT compilation. So, back to the future, again, here are some guidelines and instructions how to write AoT-friendly apps with the Angular CLI.

Kick-start a project

First, install the Angular CLI as shown below. Since we are going to use yarn as the package manager, we set a configuration for the Angular CLI. Then, we bootstrap a new project and add a simple CSS framework for the eye’s pleasure:

$ npm install -g @angular/cli
$ ng set --global packageManager yarn
$ ng new ng-aot-guide
$ cd ng-aot-guide
$ yarn add spectre.css

Now, generate some application code for demonstration purposes. Our app is fairly trivial with just one NgModule, one Component, and one Service.

$ ng generate module bttf
$ ng generate component bttf
$ ng generate service bttf/bttf

A first glance at the app:

$ ng serve

Then open http://localhost:4200 and you see a screen yelling “bttf works!

Serving and Building — w/ AoT — w/o AoT

Angular CLI comes with built-in AoT support. In the development target environment, it uses JiT (Just-in-Time) compilation for better developer experiences and faster reloads. In production target environment, AoT (Ahead-of-Time) compilation is used for best user experience and faster page loads. You specify the build target by adding the --dev or --prod flag to the ng commands.

By default, it uses the development target. Build the application:

$ ng build

The Angular CLI outputs the build artefacts in the dist folder which now looks like this:

Now, let’s build for production:

$ ng build --prod

For an overview, here are the Angular CLI commands for both development and production build targets:

$ ng serve --dev
$ ng serve --prod
$ ng build --dev
$ ng build --prod

A chicken application

To demonstrate and understand how Ahead-of-Time compilation works, we need to add some features to the application. Checkout the Git tag baseline or take a look at that commit. The running application now looks:

This user interface is implemented by a BttfComponent with a BttfService behind. Both component and service are located in BttfModule.

In JiT compilation (ng build --dev), the application works fine. However, with AoT compilation (ng build --prod), we encounter several errors. We will talk through this errors and look how to fix and avoid them.

Common Mistake #1: Factory functions must be exported, named functions

The first error message is:

Error encountered resolving symbol values statically. Function calls are not supported.
Consider replacing the function or lambda with a reference to an exported function

It is caused in bttf.module.ts by the factory provider:

{
provide: BttfService,
useFactory: () => new BttfService()
}

With AoT compilation, lambda expressions (arrow functions in TypeScript jargon) are not supported for writing factories! We have to replace the factory with a plain-old function:

export function myServiceFactory() {
return new BttfService();
}
@NgModule({
providers: [
{
provide: BttfService,
useFactory: myServiceFactory
}
]
})
export class BttfModule {}

You can see the solution in the Git tag fix-1. Take a look at it!

WARNING: the dependency injection guide / Tour of Heroes on angular.io gives a non-working example for factory providers. The document is outdated and needs to be updated!

Common Mistake #2: bound properties must be public

Now, when running ng build --prod again, another error shows up:

Property '{xyz}' is private and only accessible within class '{component}'

The error is caused by two places in bttf.component.ts and bttf.component.html. In the HTML template, we have a text interpolation binding for the headline and an event binding for the form:

<h2>{{ message }}</h2>
<form #f="ngForm" (ngSubmit)="onSubmit(f.controls.question.value)">
...
</form>

In the component class, the corresponding code snippet is:

@Component({ .. })
export class BttfComponent {

private message: string = `Back to the future, again!`;
  private onSubmit(value: string) {
/* .. */
}
}

Both the property and the method need to be public members! By simply removing the private keyword, both will be public by default. So, the fix for this error is:

@Component({ .. })
export class BttfComponent {

message: string = `Back to the future, again!`;
  onSubmit(value: string) {
/* .. */
}
}

If you like to be even more explicit, you can declare a public message: string property as well as a public onSubmit()method. The solution is shown in Git tag fix-2 — you find the commit here!

Common Mistake #3: call signatures for event bindings must match

There is one more issue with the application:

Supplied parameters do not match any signature of call target.

Again, this error is caused by BttfComponent and its template. The code part in the template is:

<button (click)="onAdd($event)">Add more {{ items }}</button>

And its counter-part in the component class:

@Component({ .. })
export class BttfComponent {
  onAdd() {
this.count += 1;
}
}

Notice that onAdd() does not declare a method parameter. However, in the template, we try to pass the $eventvariable to the method. This causes AoT compilation to fail and has two possible solutions. First, change the method implementation in the class to accept a paramter or remove the paramter from the event binding in the template. We choose to not pass $event to the method since it is not needed anyway. The fixed template code is:

<button (click)="onAdd()">Add more {{ items }}</button>

To see the solution in Git tag fix-3 you can look at this commit!

Building with AoT

Finally, we are able to compile the application for production with AoT enabled.

$ ng build --prod

Did you notice the difference in file sizes? With the development build, vendor.bundle.js was 1.88MB in size and main.bundle.js was 7.51kB. In the production build, vendor.bundle.js is reduced to 1.07MB and main.bundle.jshas grown to 23.6kB.

Here is why! First, in the production build, JavaScript files are minified by webpack. The webpack statistics print all file sizes for unminified, not-gzipped ES5 files — so minification doesn’t count in the above statistics!

There must be another reason. With AoT compilation, the Angular compiler is no longer included in the vendor bundle. The @angular/compiler package accounts for roughly 999kB in unminified ES5 code! As Minko Gechev points out, ommitting the compiler from the vendor bundle saves us quite a few KWh of energy on the planet! On top, AoT applications will run much faster in the web browser!

How is all that possible? The performance improvements are achieved by a trade-off. With AoT compilation, so-called factory code for components is generated. Angular CLI generates (intermediate) files “under the hood” and does not make them visible to the user. Since that code needs to be included in the application, the main.bundle.js is increased in file size (from 7.51kB to 23.6kB, ~3 times larger) and the build takes longer to execute (from ~26sec to ~34 sec) in the above example.

A look inside the code

To get a deep understanding of how AoT works, let’s take a look at the generated factory code. The factory code is written to*.ngfactory.ts files. Even though we cannot look at those files directly, we can output an un-minified, AoT-compiled bundle:

$ ng build --dev --aot

Now, open dist/main.bundle.js. It is still possible to recognize the most important parts in the not-so-pretty-looking code. For instance, the TypeScript class BttfComponent is compiled to the following snippet of JavaScript code (ES5 syntax). Basically, it's a JavaScript prototype with additional functions __metadata() and __decorate() applied to it. All of a sudden, does it make sense that they call @Component() a decorator?

With AoT compilation, additional component factories are generated. The code implements View classes. We can think of views as intermediaries between the browser's runtime and the component classes. Technically, this is probably way too over-simplified and does not meet the complexity of Angular's implementation details. Just for simplicity of a mental model, let's pretend that a View behaves like a glue-code proxy between the browser runtime (DOM) and components at the other end.

Here’s an example of a View generated for BttfComponent:

You find the _co.onAdd() call in the above code listing. Notice that it checks for if(('click' == en)), then checks the return value _co.onAdd() !== false, stores it in a local pd_0 variable, and evaluates and returns a boolean expression (pd_0 && ad). Well, does that look familiar to you?

Think back a few years! Did you ever write such code?

elem.addEventListener(function (evt) {
if (evt.type === 'click') {
/* do things... */
return true; // cancel the event
}
});

I used to write such code. A lot. :-)

In 2017, we don’t do these things. As good and tame Angular developers, we write:

<button (click)="onAdd($event)">Add more chicken</button>

The important thing here is that the factory code reflects the component’s HTML template in a slightly different way. It looks like the HTML markup is transformed into a set of JavaScript instructions. Actually, such a transformation is performed by the Angular AoT compiler.

We cannot see type information in the above code listing because it’s JavaScript code where type information is lost. When the compiler does the HTML-to-JavaScript transformation, it does so in two steps. First, the compiler generates factory code in TypeScript and then compiles both the *.ngfactory.ts and *.component.ts files to JavaScript.

This means that the AoT compiler performs type checking from template expressions in HTML. A template is transformed into TypeScript code. That code is compiled and depends on the component class. So how could a factory invoke a private onAdd() method with the parameter $event? It simply cannot and throws a type error!

Now, we see: we fixed exactly these kind of errors in the previous sections!

Reading Further

The example code shown in this article is available on GitHub!

Here are some additional resources on Angular’s AoT compilation.

At ng-conf, Tobias Bosch spoke about the Angular compiler in detail. You can watch him explain how bindings and view definitions work!

https://youtu.be/RXYjPYkFwy4?t=6m45s

Also, I wrote on experiences with AoT compilation in Angular v2 previously:

And last, don’t try too get too much ahead-of-time! Time travel doesn’t work yet — even with the DeLorean!