Why AOT Matters
Angular's template compilation is a key factor in application performance. The default approach, Just in Time (JIT) compilation, happens in the user's browser after all scripts are delivered. A faster startup can be achieved by compiling those templates during the build itself, an approach known as Ahead of Time (AOT) compilation. A significant benefit is that the Angular compiler itself doesn't need to be shipped to the browser, which trims the framework's footprint and shrinks the final download size.
The TypeScript output from AOT is also inherently static, enabling tools to analyze exactly which parts of Angular or third-party libraries are actually imported. This is where tree shaking, implemented by tools like webpack2 or rollup, becomes valuable; these tools prune unused exports, eliminating dead code from the final bundle.
This post walks through a practical example from my GitHub repository to illustrate how to set up both techniques in tandem.
Setting Up the Build Pipeline
The demonstration project here starts by invoking the Angular Compiler to transform application templates into TypeScript classes. Next, that generated code, together with the rest of the TypeScript source, is transpiled down to EcmaScript 5. To prepare for tree shaking, the build takes advantage of the EcmaScript 2015 module system, which introduces the import and export statements. These are crucial as they provide the static structure that makes it possible for bundlers to detect and remove unused modules:
After these steps, the sample pipeline engages either Rollup or webpack2 to perform the actual tree shaking. Both can also be paired with UglifyJS to minify the output. Notably, webpack2's tree shaking is a bit different: it marks code as unused, and relies on a minifier like UglifyJS to strip it out. Since UglifyJS only understands EcmaScript 5 at this point, the code must be transpiled to that version first. Another reason for this transpilation is webpack2's own limitations with newer syntax. Finally, EcmaScript 5 is the most widely available format for npm libraries, though it can still be used alongside the ES2015 module syntax that tree shaking requires. This combination is what enables the whole optimization.
As discussed elsewhere, starting from a newer language version like EcmaScript 2015 would yield even better optimization opportunities due to richer static analysis. The final output from either bundler is a tree-shaken, minified bundle.
Necessary Code Adjustments
One of the main restrictions of AOT compilation is its inability to handle dynamic module references. This means that using require is off the table. During development with JIT, the angular2-template-loader for Webpack is handy because it inlines template and style URLs from Angular components, but the AOT compiler itself can also process these relative paths:
@Component({
selector: 'flug-suchen',
templateUrl: './flug-suchen.component.html',
styleUrls: ['./flug-suchen.component.css'],
})
export class FlugSuchenComponent {
[...]
}
The stricter nature of AOT will often surface new compile errors. These errors can be fixed by iterating on the compiler feedback. Oliver Combe, who maintains the ng2-translate library, maintains a compatibility list of features that do not work with the AOT Compiler. Another authoritative resource about this is available as a gist.
To catch these issues earlier in the development cycle, Angular 2.3 introduced the Angular Language Service. This tool integrates with editors and IDEs to provide real-time, Angular-aware code completion and error checking in both TypeScript and HTML template files. Support for this was added in WebStorm 2017.1, and a plugin for Visual Studio Code is currently in preview.
Project Dependencies
Setting up a project for AOT along with tree shaking by Webpack2 and Rollup requires installing the right packages. Here is a list of the dev-dependencies used in this example:
"angular2-template-loader": "^0.6.0",
"awesome-typescript-loader": "~3.0.0-beta.18",
"css-loader": "^0.26.0",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.21.0",
"webpack": "^2.2.0",
"webpack-dev-server": "^2.2.0",
"webpack-dll-bundles-plugin": "^1.0.0-beta.2"
"rollup": "^0.41.4",
"rollup-plugin-commonjs": "^7.0.0",
"rollup-plugin-node-globals": "^1.1.0",
"rollup-plugin-node-resolve": "^2.0.0",
"rollup-plugin-uglify": "^1.0.1",
The libraries compiler-cli and plattform-server are also needed as regular dependencies:
"@angular/compiler-cli": "~2.4.0",
"@angular/platform-server": "~2.4.0",
The complete list of dev-dependencies is included within the GitHub sample.
Compiler Configuration File
The AOT compiler reads its settings from a dedicated tsconfig.json-file. Following the official docs, the sample names this file tsconfig.aot.json:
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist/unbundled-aot",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
"angularCompilerOptions": {
"genDir": "aot",
"skipMetadataEmit" : true
}
}
As previously mentioned, setting the target to es5 while using the es2015 module format is essential for tree shaking to work correctly. Under angularCompilerOptions, the genDir property specifies the folder where the template compiler will write the generated files derived from HTML. The configuration also instructs the compiler to skip generating metadata files (which are only relevant for publishing reusable libraries).
The outDir property determines that the transpiled ES5 output should go into the dist/unbundled-aot directory. Both Webpack2 and Rollup are pointed to this location.
Generating Template Code
The package.json includes an npm script that runs the compiler to process the HTML templates, generating TypeScript files for them. It then proceeds to compile the whole project down to ES5:
"ngc": "ngc -p tsconfig.aot.json",
Execute this process by running npm run ngc in the terminal.
Booting an AOT Application
The application startup for an AOT-compiled Angular app is different from the JIT mode. Instead of the JIT bootstrap, the root module must be launched using platformBrowser().bootstrapModuleFactory:
// main.aot.ts
import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory).catch(err => console.error(err));
Here, AppModuleNgFactory is the generated factory class for the root module AppModule, created by the AOT compiler. Remember to re-run the ngc command whenever the main module is created or updated.
Webpack2 Setup for AOT
The webpack2 configuration for the AOT build is relatively straightforward. Note the two entry points in the dist/unbundled-aot folder: one for the standard polyfills and one pointing to the compiled version of the main.aot.ts file described earlier:
var webpack = require('webpack');
var CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
profile: true,
devtool: false,
entry: {
'polyfills': './dist/unbundled-aot/app/polyfills.js',
'app': './dist/unbundled-aot/app/main.aot.js'
},
output: {
path: __dirname + "/dist/aot",
filename: "[name].js",
publicPath: "dist/"
},
resolve: {
extensions: [/*'.ts',*/ '.js', '.jpg', '.jpeg', '.gif', '.png', '.css', '.html']
},
module: {
loaders: [
{ test: /\.(jpg|jpeg|gif|png)$/, loader:'file-loader?name=img/[path][name].[ext]' },
{ test: /\.(eof|woff|woff2|svg)$/, loader:'file-loader?name=img/[path][name].[ext]' },
{ test: /\.css$/, loader:'raw-loader' },
{ test: /\.html$/, loaders: ['html-loader'] }
],
exprContextCritical: false
},
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: {
comments: false
},
sourceMap: false
}),
new CompressionPlugin({
asset: "[path].gz[query]",
algorithm: "gzip",
test: /\.js$|\.html$/,
threshold: 10240,
minRatio: 0.8
})
],
node: {
__filename: true
},
devServer: {
inline:true,
port: 8080,
historyApiFallback: true,
watchOptions: {
aggregateTimeout: 300,
poll: 1000
}
}
};
Both the LoaderOptionsPlugin and UglifyJsPlugin are configured to optimize the bundle. They work together to achieve minification and tree shaking, stripping the bundle down to its minimal size.
Webpack2 Setup for Development
For a fast feedback loop during development, the project includes a JIT-based build. This is preferable because building for JIT is significantly quicker than for AOT. The webpack configuration is mostly identical, but with a couple key differences. First off, the main entry point resolves to the main.jit.ts file which uses the traditional JIT bootstrap. The other difference is the reliance on the angular2-template-loader to inline templates and the awesome-typescript-loader for transpiling the TypeScript code.
[...]
var webpack = require('webpack');
var CompressionPlugin = require("compression-webpack-plugin");
var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
module.exports = {
devtool: false,
entry: {
'polyfills': './app/polyfills.ts',
'app': './app/main.jit.ts'
},
output: {
path: __dirname + "/dist/jit",
filename: "[name].js",
publicPath: "dist/"
},
resolve: {
extensions: ['.ts', '.js', '.jpg', '.jpeg', '.gif', '.png', '.css', '.html']
},
module: {
loaders: [
{ test: /\.(jpg|jpeg|gif|png)$/, loader:'file-loader?name=img/[path][name].[ext]' },
{ test: /\.(eof|woff|woff2|svg)$/, loader:'file-loader?name=img/[path][name].[ext]' },
{ test: /\.css$/, loader:'raw-loader' },
{ test: /\.html$/, loaders: ['raw-loader'] },
{ test: /\.ts$/, loaders: ['angular2-template-loader', 'awesome-typescript-loader'], exclude: /node_modules/}
],
exprContextCritical: false
},
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: {
comments: false
},
sourceMap: false
}),
new CompressionPlugin({
asset: "[path].gz[query]",
algorithm: "gzip",
test: /\.js$|\.html$/,
threshold: 10240,
minRatio: 0.8
})
],
node: {
__filename: true
},
devServer: {
inline:true,
port: 8080,
historyApiFallback: true,
watchOptions: {
aggregateTimeout: 300,
poll: 1000
}
}
};
[...]
Rollup for AOT Bundling
To see if Rollup could offer smaller bundles than webpack2, the sample adopted the configuration from the official AOT and Rollup cookbook on angular.io:
// rollup.config.js
import nodeResolve from 'rollup-plugin-node-resolve'
import uglify from 'rollup-plugin-uglify'
export default {
entry: 'dist/unbundled-aot/app/main.aot.js',
dest: 'dist/build.js', // output a single application bundle
sourceMap: false,
treeshake: true,
format: 'iife',
onwarn: function(warning) {
// Skip certain warnings
if ( warning.code === 'THIS_IS_UNDEFINED' ) { return; }
if ( warning.indexOf("The 'this' keyword is equivalent to 'undefined'") > -1 ) { return; }
console.warn( warning.message );
},
plugins: [
nodeResolve({jsnext: true, module: true}),
commonjs({
include: 'node_modules/rxjs/**',
}),
uglify()
]
}
Like the webpack approach, this also points to the ES5 files in dist/unbundled-aot which use the EcmaScript 2015 module conventions.
Build Automation
The project defines npm scripts to build for every scenario mentioned:
"build-all": "npm run webpack:jit && npm run webpack:aot && npm run rollup:aot",
"webpack:aot": "ngc -p tsconfig.aot.json && webpack --config webpack.aot.config.js",
"webpack:jit": "webpack --config webpack.jit.config.js",
"rollup:aot": "ngc -p tsconfig.aot.json && rollup -c rollup.js",
All three builds can be triggered sequentially with the single command npn run build-all.
Performance Gains
The JIT variant produced a bundle with a size of 848 kB:
Asset Size Chunks Chunk Names
app.js 848 kB 0 [emitted] [big] app
polyfills.js 103 kB 1 [emitted] polyfills
polyfills.js.gz 33.5 kB [emitted]
app.js.gz 199 kB [emitted]
Switching to AOT with webpack2 reduced the total output:
Asset Size Chunks Chunk Names
app.js 531 kB 0 [emitted] [big] app
polyfills.js 103 kB 1 [emitted] polyfills
polyfills.js.gz 33.5 kB [emitted]
app.js.gz 112 kB [emitted]
An even smaller size was achieved by bundling the AOT output with Rollup:
470.978 build.js
After gzip compression, the resulting file was just 94 kB:
94.337 build.js.gz
Running the Examples
Three runnable examples are set up as distinct HTML entry points: index.jit.html loads the dev build, index.aot.html serves the AOT and webpack bundle, and index.rollup.aot.html uses the Rollup output. These all rely on the bundle files found in the project.

