This cookbook describes how to radically improve performance by compiling Ahead of Time (AoT) during a build process.
Table of Contents
- Overview
- Ahead-of-Time vs Just-in-Time
- Compile with AoT
- Bootstrap
- Tree Shaking
- Load the bundle
- Serve the app
- Source Code
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:
<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>
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 toplatformBrowser().bootstrapModuleFactory
and pass in the AppModuleNgFactory
.
Here is AoT bootstrap in main.ts
next to the familiar JiT version:
import { platformBrowser } from '@angular/platform-browser'; import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory'; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
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:
<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>
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; } }
import { platformBrowser } from '@angular/platform-browser'; import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory'; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
<!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>
{ "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 } }
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() ] }
Please login to continue.