Ahead-of-Time Compilation

This cookbook describes how to radically improve performance by compiling Ahead of Time (AoT) during a build process.

Table of Contents

Overview

Angular component templates consist of a mix of standard html and Angular syntax (e.g. ngIf, ngFor).

Expressions like ngIf and ngFor are specific to Angular. The browser cannot execute them directly.

Before the browser can render the application, Angular components and their templates must be converted to executable JavaScript. We refer to this step as Angular compilation or just plain compilation.

You can compile the app in the browser, at runtime, as the application loads, using the Just-in-Time (JiT) compiler. This is the standard development approach shown throughout the documentation.

JiT compilation incurs a runtime performance penalty. Views take longer to render because of the in-browser compilation step. The application is bigger because it includes the Angular compiler and a lot of library code that the application won't actually need. Bigger apps take longer to transmit and are slower to load.

This cookbook describes how to improve performance by compiling at build time instead, using a process called Ahead-of-Time compilation (AoT).

Ahead-of-time (AoT) vs Just-in-time (JiT)

There is actually only one Angular compiler. The difference between AoT and JiT is a matter of timing and tooling. With AoT, the compiler runs once at build time using one set of libraries; With JiT it runs every time for every user at runtime using a different set of libraries.

Why do AoT compilation?

The performance improvement from doing AoT compilation can be significant for three reasons:

Faster rendering

With AoT, the browser downloads a pre-compiled version of the application. The browser loads executable code so it can render the application immediately, without waiting to compile the app first.

Fewer asynchronous requests

The compiler inlines external html templates and css style sheets within the application JavaScript, eliminating separate ajax requests for those source files.

Smaller Angular framework download size

There's no need to download the Angular compiler if the app is already compiled. The compiler is roughly half of Angular itself, so omitting it dramatically reduces the application payload.

Compile with AoT

Prepare for offline compilation

This cookbook takes the QuickStart as its starting point. A few minor changes to the lone app.component lead to these two class and html files:

app/app.component.html
<button (click)="toggleHeading()">Toggle Heading</button>
<h1 *ngIf="showHeading">My First Angular 2 App</h1>

<h3>List of Heroes</h3>
<div *ngFor="let hero of heroes">{{hero}}</div>
app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  showHeading = true;
  heroes = ['Magneta', 'Bombasto', 'Magma', 'Tornado'];

  toggleHeading() {
    this.showHeading = !this.showHeading;
  }

}

Install a few new npm dependencies with the following command:

npm install @angular/compiler-cli @angular/platform-server --save

You will run the ngc compiler provided in the @angular/compiler-cli npm package instead of the TypeScript compiler (tsc).

ngc is a drop-in replacement for tsc and is configured much the same way.

ngc requires its own tsconfig.json with AoT-oriented settings. Copy the original tsconfig.json to a file called tsconfig-aot.json, then modify it to look as follows.

tsconfig-aot.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  },

  "files": [
    "app/app.module.ts",
    "app/main.ts",
    "./typings/index.d.ts"
  ],

  "angularCompilerOptions": {
   "genDir": "aot",
   "skipMetadataEmit" : true
 }
}

The compilerOptions section is unchanged except for one property. Set the module to es2015. This is important as explained later in the Tree Shaking section.

What's really new is the ngc section at the bottom called angularCompilerOptions. Its "genDir" property tells the compiler to store the compiled output files in a new aot folder.

The "skipMetadataEmit" : true property prevents the compiler from generating metadata files with the compiled application. Metadata files are not necessary when targeting TypeScript files, so there is no reason to include them.

Compiling the application

Initiate AoT compilation from the command line using the previously installed ngc compiler by executing:

node_modules/.bin/ngc -p tsconfig-aot.json

ngc expects the -p switch to point to a tsconfig.json file or a folder containing a tsconfig.json file.

After ngc completes, look for a collection of NgFactory files in the aot folder (the folder specified as genDir in tsconfig-aot.json).

These factory files are essential to the compiled application. Each component factory creates an instance of the component at runtime by combining the original class file and a JavaScript representation of the component's template. Note that the original component class is still referenced internally by the generated factory.

The curious can open the aot/app.component.ngfactory.ts to see the original Angular template syntax in its intermediate, compiled-to-TypeScript form.

JiT compilation generates these same NgFactories in memory where they are largely invisible. AoT compilation reveals them as separate, physical files.

Do not edit the NgFactories! Re-compilation replaces these files and all edits will be lost.

Bootstrap

The AoT path changes application bootstrapping.

Instead of bootstrapping AppModule, you bootstrap the application with the generated module factory, AppModuleNgFactory.

Switch from the platformBrowserDynamic.bootstrap used in JiT compilation to
platformBrowser().bootstrapModuleFactory and pass in the AppModuleNgFactory.

Here is AoT bootstrap in main.ts next to the familiar JiT version:

app/main.ts (AoT)
import { platformBrowser }    from '@angular/platform-browser';

import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
app/main.ts (JiT)
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule }              from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

Be sure to recompile with ngc!

Tree Shaking

AoT compilation sets the stage for further optimization through a process called Tree Shaking. A Tree Shaker walks the dependency graph, top to bottom, and shakes out unused code like dead needles in a Christmas tree.

Tree Shaking can greatly reduce the downloaded size of the application by removing unused portions of both source and library code. In fact, most of the reduction in small apps comes from removing unreferenced Angular features.

For example, this demo application doesn't use anything from the @angular/forms library. There is no reason to download Forms-related Angular code and tree shaking ensures that you don't.

Tree Shaking and AoT compilation are separate steps. Tree Shaking can only target JavaScript code. AoT compilation converts more of the application to JavaScript, which in turn makes more of the application "Tree Shakable".

Rollup

This cookbook illustrates a Tree Shaking utility called Rollup.

Rollup statically analyzes the application by following the trail of import and export statements. It produces a final code bundle that excludes code that is exported, but never imported.

Rollup can only Tree Shake ES2015 modules which have import and export statements.

Recall that tsconfig-aot.json is configured to produce ES2015 modules. It's not important that the code itself be written with ES2015 syntax such as class and const. What matters is that the code uses ES import and export statements rather than require statements.

Install the Rollup dependencies with this command:

npm install rollup rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-uglify --save-dev

Next, create a configuration file to tell Rollup how to process the application. The configuration file in this cookbook is named rollup.js and looks like this.

rollup.js

import rollup      from 'rollup'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs    from 'rollup-plugin-commonjs';
import uglify      from 'rollup-plugin-uglify'

export default {
  entry: 'app/main.js',
  dest: 'dist/build.js', // output a single application bundle
  sourceMap: false,
  format: 'iife',
  plugins: [
      nodeResolve({jsnext: true, module: true}),
      commonjs({
        include: 'node_modules/rxjs/**',
      }),
      uglify()
  ]
}

It tells Rollup that the app entry point is app/main.js . The dest attribute tells Rollup to create a bundle called build.js in the dist folder. Then there are plugins.

Rollup Plugins

Optional plugins filter and transform the Rollup inputs and output.

RxJS Rollup expects application source code to use ES2015 modules. Not all external dependencies are published as ES2015 modules. In fact, most are not. Many of them are published as CommonJS modules.

The RxJs observable library is an essential Angular dependency published as an ES5 JavaScript CommonJS module.

Luckily there is a Rollup plugin that modifies RxJs to use the ES import and export statements that Rollup requires. Rollup then preserves in the final bundle the parts of RxJS referenced by the application.

rollup.js (CommonJs to ES2015 Plugin)

commonjs({
  include: 'node_modules/rxjs/**',
}),

Minification

Rollup Tree Shaking reduces code size considerably. Minification makes it smaller still. This cookbook relies on the uglify Rollup plugin to minify and mangle the code.

rollup.js (CommonJs to ES2015 Plugin)

uglify()

In a production setting, you would also enable gzip on the web server to compress the code into an even smaller package going over the wire.

Run Rollup

Execute the Rollup process with this command:

node_modules/.bin/rollup -c rollup.js

Rollup may log many lines with the following warning message:

The `this` keyword is equivalent to `undefined` at the top level of an ES module, and has been rewritten

You can safely ignore these warnings.

Load the Bundle

Loading the generated application bundle does not require a module loader like SystemJS. Remove the scripts that concern SystemJS. Instead, load the bundle file using a single script tag:

index.html (load bundle)

<body>
  <my-app>Loading...</my-app>
</body>

<script src="dist/build.js"></script>

Serve the app

You'll need a web server to host the application. Use the same Lite Server employed elsewhere in the documentation:

npm run lite

The server starts, launches a browser, and the app should appear.

Source Code

Here is the pertinent AoT source code for this cookbook:

app/app.component.html
<button (click)="toggleHeading()">Toggle Heading</button>
<h1 *ngIf="showHeading">My First Angular 2 App</h1>

<h3>List of Heroes</h3>
<div *ngFor="let hero of heroes">{{hero}}</div>
app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  showHeading = true;
  heroes = ['Magneta', 'Bombasto', 'Magma', 'Tornado'];

  toggleHeading() {
    this.showHeading = !this.showHeading;
  }

}
app/main.ts
import { platformBrowser }    from '@angular/platform-browser';

import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Ahead of time compilation</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="styles.css">

    <script src="node_modules/core-js/client/shim.min.js"></script>
    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>

  </head>

  <body>
    <my-app>Loading...</my-app>
  </body>

  <script src="dist/build.js"></script>
</html>
tsconfig-aot.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  },

  "files": [
    "app/app.module.ts",
    "app/main.ts",
    "./typings/index.d.ts"
  ],

  "angularCompilerOptions": {
   "genDir": "aot",
   "skipMetadataEmit" : true
 }
}
rollup.js
import rollup      from 'rollup'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs    from 'rollup-plugin-commonjs';
import uglify      from 'rollup-plugin-uglify'

export default {
  entry: 'app/main.js',
  dest: 'dist/build.js', // output a single application bundle
  sourceMap: false,
  format: 'iife',
  plugins: [
      nodeResolve({jsnext: true, module: true}),
      commonjs({
        include: 'node_modules/rxjs/**',
      }),
      uglify()
  ]
}
doc_Angular
2016-10-06 09:46:09
Comments
Leave a Comment

Please login to continue.