Ahead-of-Time Compilation

This cookbook describes how to radically improve performance by compiling ahead-of-time (AOT) during a build process.

Overview

An Angular application consists largely of components and their HTML templates. Before the browser can render the application, the components and templates must be converted to executable JavaScript by the Angular compiler.

Watch compiler author Tobias Bosch explain the Angular Compiler at AngularConnect 2016.

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. It's great but it has shortcomings.

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.

Compilation can uncover many component-template binding errors. JIT compilation discovers them at runtime, which is late in the process.

The ahead-of-time (AOT) compiler can catch template errors early and improve performance by compiling at build time.

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?

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.

Detect template errors earlier

The AOT compiler detects and reports template binding errors during the build step before users can see them.

Better security

AOT compiles HTML templates and components into JavaScript files long before they are served to the client. With no templates to read and no risky client-side HTML or JavaScript evaluation, there are fewer opportunities for injection attacks.

Compile with AOT

Preparing for offline compilation takes a few simple steps. Take the Setup as a starting point. A few minor changes to the lone app.component lead to these two class and HTML files:

src/app/app.component.html
<button (click)="toggleHeading()">Toggle Heading</button>
<h1 *ngIf="showHeading">Hello Angular</h1>

<h3>List of Heroes</h3>
<div *ngFor="let hero of heroes">{{hero}}</div>
src/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 src/tsconfig.json to a file called tsconfig-aot.json on the project root, then modify it as follows.

tsconfig-aot.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es2015", "dom"],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "typeRoots": [
      "./node_modules/@types/"
    ]
  },

  "files": [
    "src/app/app.module.ts",
    "src/main.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.

Component-relative template URLS

The AOT compiler requires that @Component URLS for external templates and CSS files be component-relative. That means that the value of @Component.templateUrl is a URL value relative to the component class file. For example, an 'app.component.html' URL means that the template file is a sibling of its companion app.component.ts file.

While JIT app URLs are more flexible, stick with component-relative URLs for compatibility with AOT compilation.

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

Windows users should surround the ngc command in double quotes:

"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 aot folder is the directory 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 aot/app.component.ngfactory.ts to see the original Angular template syntax compiled to TypeScript, its intermediate 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 approach changes application bootstrapping.

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

Make a copy of main.ts and name it main-jit.ts. This is the JIT version; set it aside as you may need it later.

Open main.ts and convert it to AOT compilation. Switch from the platformBrowserDynamic.bootstrap used in JIT compilation to platformBrowser().bootstrapModuleFactory and pass in the AOT-generated AppModuleNgFactory.

Here is AOT bootstrap in main.ts next to the original JIT version:

src/main.ts
import { platformBrowser }    from '@angular/platform-browser';
import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory';

console.log('Running AOT compiled');
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
src/main-jit.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule }              from './app/app.module';

console.log('Running JIT compiled');
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 leaves in a 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.

In the terminal window, 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 (rollup-config.js) in the project root directory to tell Rollup how to process the application. The cookbook configuration file looks like this.

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

export default {
  entry: 'src/main.js',
  dest: 'src/build.js', // output a single application bundle
  sourceMap: false,
  format: 'iife',
  onwarn: function(warning) {
    // Skip certain warnings

    // should intercept ... but doesn't in some rollup versions
    if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }

    // console.warn everything else
    console.warn( warning.message );
  },
  plugins: [
      nodeResolve({jsnext: true, module: true}),
      commonjs({
        include: 'node_modules/rxjs/**',
      }),
      uglify()
  ]
};

This config file tells Rollup that the app entry point is src/app/main.js . The dest attribute tells Rollup to create a bundle called build.js in the dist folder. It overrides the default onwarn method in order to skip annoying messages about the AOT compiler's use of the this keyword.

The next section covers the plugins in more depth.

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 the parts of RxJS referenced by the application in the final bundle. Using it is straigthforward. Add the following to the plugins array in rollup-config.js:

rollup-config.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. Add the following to the plugins array:

rollup-config.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-config.js

Windows users should surround the rollup command in double quotes:

"node_modules/.bin/rollup"  -c rollup-config.js

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 after the </body> tag:

index.html (load bundle)
<script src="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.

AOT QuickStart source code

Here's the pertinent source code:

src/app/app.component.html
<button (click)="toggleHeading()">Toggle Heading</button>
<h1 *ngIf="showHeading">Hello Angular</h1>

<h3>List of Heroes</h3>
<div *ngFor="let hero of heroes">{{hero}}</div>
src/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;
  }
}
src/main.ts
import { platformBrowser }    from '@angular/platform-browser';
import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory';

console.log('Running AOT compiled');
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
src/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Ahead of time compilation</title>
    <base href="/">
    <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>
  </head>
  <body>
    <my-app>Loading...</my-app>
  </body>
  <script src="build.js"></script>
</html>
tsconfig-aot.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es2015", "dom"],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "typeRoots": [
      "./node_modules/@types/"
    ]
  },

  "files": [
    "src/app/app.module.ts",
    "src/main.ts"
  ],

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

export default {
  entry: 'src/main.js',
  dest: 'src/build.js', // output a single application bundle
  sourceMap: false,
  format: 'iife',
  onwarn: function(warning) {
    // Skip certain warnings

    // should intercept ... but doesn't in some rollup versions
    if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }

    // console.warn everything else
    console.warn( warning.message );
  },
  plugins: [
      nodeResolve({jsnext: true, module: true}),
      commonjs({
        include: 'node_modules/rxjs/**',
      }),
      uglify()
  ]
};

Workflow and convenience script

You'll rebuild the AOT version of the application every time you make a change. Those npm commands are long and difficult to remember.

Add the following npm convenience script to the package.json so you can compile and rollup in one command.

"build:aot": "ngc -p tsconfig-aot.json && rollup -c rollup-config.js",

Open a terminal window and try it.

npm run build:aot

Develop JIT along with AOT

AOT compilation and rollup together take several seconds. You may be able to develop iteratively a little faster with SystemJS and JIT. The same source code can be built both ways. Here's one way to do that.

  • Make a copy of index.html and call it index-jit.html.
  • Delete the script at the bottom of index-jit.html that loads bundle.js
  • Restore the SystemJS scripts like this:
src/index-jit.html (SystemJS scripts)
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
  System.import('main-jit.js').catch(function(err){ console.error(err); });
</script>

Notice the slight change to the system.import which now specifies src/app/main-jit. That's the JIT version of the bootstrap file that we preserved above.

Open a different terminal window and enter npm start.

npm start

That compiles the app with JIT and launches the server. The server loads index.html which is still the AOT version, which you can confirm in the browser console. Change the address bar to index-jit.html and it loads the JIT version. This is also evident in the browser console.

Develop as usual. The server and TypeScript compiler are in "watch mode" so your changes are reflected immediately in the browser.

To see those changes in AOT, switch to the original terminal and re-run npm run build:aot. When it finishes, go back to the browser and use the back button to return to the AOT version in the default index.html.

Now you can develop JIT and AOT, side-by-side.

Tour of Heroes

The sample above is a trivial variation of the QuickStart application. In this section you apply what you've learned about AOT compilation and tree shaking to an app with more substance, the Tour of Heroes application.

JIT in development, AOT in production

Today AOT compilation and tree shaking take more time than is practical for development. That will change soon. For now, it's best to JIT compile in development and switch to AOT compilation before deploying to production.

Fortunately, the source code can be compiled either way without change if you account for a few key differences.

index.html

The JIT and AOT apps require their own index.html files because they setup and launch so differently.

Here they are for comparison:

aot/index.html (AOT)
<!DOCTYPE html>
<html>
  <head>
    <base href="/">
    <title>Angular Tour of Heroes</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="styles.css">

    <script src="shim.min.js"></script>
    <script src="zone.min.js"></script>
  </head>

  <body>
    <my-app>Loading...</my-app>
  </body>
  <script src="dist/build.js"></script>
</html>
src/index.html (JIT)
<!DOCTYPE html>
<html>
  <head>
    <base href="/">
    <title>Angular Tour of Heroes</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="styles.css">

    <!-- Polyfills -->
    <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/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js').catch(function(err){ console.error(err); });
    </script>
  </head>

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

The JIT version relies on SystemJS to load individual modules. Its scripts appear in its index.html.

The AOT version loads the entire application in a single script, aot/dist/build.js. It does not need SystemJS, so that script is absent from its index.html

main.ts

JIT and AOT applications boot in much the same way but require different Angular libraries to do so. The key differences, covered in the Bootstrap section above, are evident in these main files which can and should reside in the same folder:

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

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

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

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

platformBrowserDynamic().bootstrapModule(AppModule);

TypeScript configuration

JIT-compiled applications transpile to commonjs modules. AOT-compiled applications transpile to ES2015/ES6 modules to facilitate tree shaking. AOT requires its own TypeScript configuration settings as well.

You'll need separate TypeScript configuration files such as these:

tsconfig-aot.json (AOT)
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es2015", "dom"],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "typeRoots": [
      "./node_modules/@types/"
    ]
  },

  "files": [
    "src/app/app.module.ts",
    "src/main-aot.ts"
  ],

  "angularCompilerOptions": {
    "genDir": "aot",
    "skipMetadataEmit" : true
  }
}
src/tsconfig.json (JIT)
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}
`@types` and node modules

In the file structure of this particular sample project, the node_modules folder happens to be two levels up from the project root. Therefore, "typeRoots" must be set to "../../node_modules/@types/".

In a more typical project, node_modules would be a sibling of tsconfig-aot.json and "typeRoots" would be set to "node_modules/@types/". Edit your tsconfig-aot.json to fit your project's file structure.

Tree shaking

Rollup does the tree shaking as before.

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

//paths are relative to the execution path
export default {
  entry: 'src/main-aot.js',
  dest: 'aot/dist/build.js', // output a single application bundle
  sourceMap: true,
  sourceMapFile: 'aot/dist/build.js.map',
  format: 'iife',
  onwarn: function(warning) {
    // Skip certain warnings

    // should intercept ... but doesn't in some rollup versions
    if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }

    // console.warn everything else
    console.warn( warning.message );
  },
  plugins: [
    nodeResolve({jsnext: true, module: true}),
    commonjs({
      include: ['node_modules/rxjs/**']
    }),
    uglify()
  ]
}

Running the application

The general audience instructions for running the AOT build of the Tour of Heroes app are not ready.

The following instructions presuppose that you have downloaded the Tour of Heroes' zip and run npm install on it.

Run the JIT-compiled app with npm start as for all other JIT examples.

Compiling with AOT presupposes certain supporting files, most of them discussed above.

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <base href="/">
    <title>Angular Tour of Heroes</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="styles.css">

    <!-- Polyfills -->
    <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/systemjs/dist/system.src.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import('main.js').catch(function(err){ console.error(err); });
    </script>
  </head>

  <body>
    <my-app>Loading...</my-app>
  </body>
</html>
copy-dist-files.js
var fs = require('fs');
var resources = [
  'node_modules/core-js/client/shim.min.js',
  'node_modules/zone.js/dist/zone.min.js',
  'src/styles.css'
];
resources.map(function(f) {
  var path = f.split('/');
  var t = 'aot/' + path[path.length-1];
  fs.createReadStream(f).pipe(fs.createWriteStream(t));
});
rollup-config.js
import rollup      from 'rollup'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs    from 'rollup-plugin-commonjs';
import uglify      from 'rollup-plugin-uglify'

//paths are relative to the execution path
export default {
  entry: 'src/main-aot.js',
  dest: 'aot/dist/build.js', // output a single application bundle
  sourceMap: true,
  sourceMapFile: 'aot/dist/build.js.map',
  format: 'iife',
  onwarn: function(warning) {
    // Skip certain warnings

    // should intercept ... but doesn't in some rollup versions
    if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }

    // console.warn everything else
    console.warn( warning.message );
  },
  plugins: [
    nodeResolve({jsnext: true, module: true}),
    commonjs({
      include: ['node_modules/rxjs/**']
    }),
    uglify()
  ]
}
tsconfig-aot.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es2015", "dom"],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true,
    "typeRoots": [
      "./node_modules/@types/"
    ]
  },

  "files": [
    "src/app/app.module.ts",
    "src/main-aot.ts"
  ],

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

With the following npm script in the scripts section of the package.json, you can easily serve the AOT-compiled application:

"serve:aot": "lite-server -c bs-config.aot.json",

Copy the AOT distribution files into the /aot folder with the node script:

node copy-dist-files

You won't do that again until there are updates to zone.js or the core-js shim for old browsers.

Now AOT-compile the app and launch:

npm run build:aot && npm run serve:aot

Inspect the Bundle

It's fascinating to see what the generated JavaScript bundle looks like after Rollup. The code is minified, so you won't learn much from inspecting the bundle directly. But the source-map-explorer tool can be quite revealing.

Install it:

npm install source-map-explorer --save-dev

Run the following command to generate the map.

node_modules/.bin/source-map-explorer aot/dist/build.js

The source-map-explorer analyzes the source map generated with the bundle and draws a map of all dependencies, showing exactly which application and NgModules and classes are included in the bundle.

Here's the map for Tour of Heroes.

toh-pt6-bundle

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v4.angular.io/guide/aot-compiler