AOT Compilation With Bundling in Angular
We discuss the differences between JIT and AOT compilation before demonstrating how to build AOT into your Angular-based application using Webpack.
Join the DZone community and get the full member experience.
Join For FreeIn the world of web development, developers enjoy developing SPAs (Single Page Applications) using Angular. One of the main reasons behind this is the modular approach of Angular, with the help of which we can separate our client-side scripting codes, like MVC patterns, from the rest of the codebase. But, the most problematic issue is that adding all the JavaScript files into our index.html page is really very painful. Not only that, it decreases the performance of the application since, while loading the index.html file, it tries to download all the JavaScript files, including related HTML files. So, if we assume that our application contains more than 500 components, then we can clearly think of how many files need to download at the beginning. Not only the files related to the components which we developed for the application, but index files download the environmental files related to Angular from the Node Modules. So, in this way, it increases the application network traffic and decreases the performance. So, the most preferred solution is that if we can bundle all the files into a single file, just like other JavaScript-based applications or MVC applications. So, to achieve this, we need to change the compilation process of our Angular applications.
JIT (Just In Time) Compilation
Traditional Angular applications (i.e. developed in Angular 1.x) were normally built using the JIT compilation. Maybe, the developers who mainly code using client-side languages/libraries like JavaScript/jQuery do not know about it or believe that there is no compile-time requirement in the case of script language. But, this is not true. Actually, in the case of JavaScript, there is always a compiler which compiles our code. This does not occur in the development or deployment time. Basically, it occurs on the fly, meaning just before our JavaScript code is going to be loaded by the browser. This process always takes a little bit more time than we think. This compilation process is required for the browsers because the browser does not read or understand any binary languages to execute or run. So, when we load any JavaScript files, those files must first be compiled so the browser can understand them and then execute them. So, AOT compilation process actually switches the compilation process from runtime to build time.
Now, we said that browsers do not understand binary languages. The browser always needs a JS file to execute. So, the question is, what is compiled at the build time by the AOT Compiler?
AOT (Ahead of Time) Compilation
AOT stands for Ahead of Time Compilation. AOT compilation was one the most important features introduced in Angular 2 and above versions. The process mechanism is very different in the case of AOT compilation versus JIT compilation. It was first introducing in Angular 2.0, but is now only available in version 2.3.0 and above. So, if anyone wants to use this compilation process, they need to use Angular 2.3.0 or above. Now, the main objective of AOT compilation is improving the performance of the application when it runs in the browser. Actually, the Angular framework has a compiler library package known as @angular/compiler
. If we compile the Angular application with the AOT mode, it performs the compilation of templates with the help of the compiler and then generates the TypeScript files (file names like *ngfactory.ts
, where the *
represents the actual name of the file; for example, if the component name is employee.ts
then the related file will be employee.ngfactory.ts
). Then the compiler again compiles these TypeScript files to the JavaScript files.
In this compilation process, a new type of file (*.metadata.json
) has been created for every TypeScript file. This metadata file is required to compile the components for their templates. So, the AOT compiler validates the HTML templates at build time and also raises an error if it finds any issues in our code. The most common issues are:
- Properties and methods which are used within the HTML template must be declared as public in the TypeScript class.
- In case of
*ngIf
directives, expressions must be Boolean. - In case of event binding, method signatures must be matched exactly with the TypeScript class method declarations.
WebPack
After the concept of AOT compilation, we also need to bundle our all .js files into one JavaScript file so that our application’s index page does not need to load a bunch of different files. For bundling the compiled JavaScript files, we have different tools available to us. One of the most important tools is Webpack. Webpack has a few advantages over the other tools, such:
- Webpack can provide us a module loading concept, just like Node.js can.
- The bundle file is easy to read and debug in the browser.
- Webpack can be configured by writing a config file code in ES2015.
- Webpack provides a similar development environment to the production/deployment enviroment.
- With the help of Webpack, we can install Angular using NPM (Node Package Manager).
Application Configuration
For using the bundle concept, we need to develop an Angular-based application that contains modules, components, etc. The module structure is given below:
In the above image, we can see that there are two config file that exist, namely webpack.config.js
and webpack.prod-config.js
. These two files are basically used as the webpack configuration files, which are required to build the application either in development mode or production mode. Before going into configuration file code, we first need to install Webpack using the following npm command:
npm install angular webpack --save-dev
Now, we can add two webpack config files. Basically, we can bundle our Angular application either using JIT compilation or AOT compilation. The config file webpack.config.js
is used for bundle the application with JIT compilation. To bundle the application, in this mode, we need to use the below command from the applications root directory:
npm run build
Sample Code for webpack.config.js
'use strict'
const path = require('path');
const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin;
const webpack = require('webpack');
const testHTML = path.resolve(__dirname, 'submodules', 'index.html');
module.exports = {
devtool: '',
profile: true,
entry: {
'shared/polyfills': [
'./submodules/app1/polyfills.ts',
'./submodules/app2/polyfills.ts',
'./submodules/app3/polyfills.ts'
],
'app1/firstapp': ['./submodules/app1/index.ts'],
'app2/secondapp': ['./submodules/app2/index.ts'],
'app3/thirdapp': ['./submodules/app3/index.ts']
},
output: {
path: path.join(__dirname, 'dist', 'dev'),
publicPath: '/',
filename: '[name].js',
chunkFilename: '[name].js'
},
resolve: {
extensions: ['.ts', '.js', '.json'],
modules: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'build-cache')
]
},
module: {
rules: [
{
test: /\.ts$/,
use: [
'awesome-typescript-loader',
'angular2-template-loader'
],
exclude: [/\.(spec|e2e)\.ts$/]
},
{
test: /\.html$/,
use: 'raw-loader'
},
{
test: /\.css$/,
use: [
'to-string-loader',
'css-loader'
]
},
]
},
plugins: [
new StatsPlugin('stats.json', 'verbose'),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/polyfills',
chunks: ['shared/polyfills']
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/vendors',
chunks: [
'app1/firstapp',
'app2/secondapp',
'app3/thirdapp'
],
// Move all node_modules into vendors chunk but not @angular
minChunks: module => /node_modules\/(?!@angular)/.test(module.resource)
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/angular-vendors',
chunks: [
'app1/firstapp',
'app2/secondapp',
'app3/thirdapp'
],
// Move all node_modules into vendors chunk but not @angular
minChunks: module => /node_modules\/@angular/.test(module.resource)
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/shared-modules',
chunks: [
'app1/firstapp',
'app2/secondapp',
'app3/thirdapp'
],
// Move duplicate modules to a shared-modules chunk
minChunks: 2
}),
// Specify the correct order the scripts will be injected in
new webpack.optimize.CommonsChunkPlugin({
name: [
'shared/polyfills',
'shared/vendors',
'shared/angular-vendors'
].reverse()
}),
new HtmlWebpackPlugin({
template: testHTML,
inject: false
})
],
node: {
global: true,
crypto: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
},
target: 'web'
}
Another config file, i.e. webpack.prod-config.js
, is required for bundling the applications with AOT compilation. As we all known, every Angular application needs a module bootstrap file, where we can bootstrap the Angular module. Normally, this file is named main.ts
or index.ts
. In the above code, we can see that every Angular app module folder (like app1, app2, etc.) contains another file named index.aot.ts
which is basically required to bootstrap the Angular module in the AOT compilation mode. In this file, we bootstrapped the Angular module's ngfactory
file which we created after the AOT compilations. To bundle the application, in this mode, we need to use the below command from the application's root directory:
npm run build:prod
Sample Code of Index.aot.ts
import { enableProdMode } from '@angular/core';
import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from 'submodules/app1/app.module.ngfactory';
try {
enableProdMode();
} catch (err) {
/**
* We do not care if the call to enableProdMode() failes
* because we only need one of the calls to enableProdMode()
* to succeed.
*/
if (err.message.indexOf('Cannot enable prod mode after platform setup.') === -1)
console.error(err);
}
platformBrowser()
.bootstrapModuleFactory(AppModuleNgFactory);
Sample Code of webpack.prod-config.ts
'use strict'
const path = require('path');
const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin;
const webpack = require('webpack');
const testHTML = path.resolve(__dirname, 'submodules', 'index.html');
module.exports = {
devtool: '',
profile: true,
entry: {
'shared/polyfills': [
'./submodules/app1/polyfills.ts',
'./submodules/app2/polyfills.ts',
'./submodules/app3/polyfills.ts'
],
'app1/app': ['./submodules/app1/index.aot.ts'],
'app2/app': ['./submodules/app2/index.aot.ts'],
'app3/app': ['./submodules/app3/index.aot.ts']
},
output: {
path: path.join(__dirname, 'dist', 'prod'),
publicPath: '/',
filename: '[name].js',
chunkFilename: '[name].js'
},
resolve: {
extensions: ['.ts', '.js', '.json'],
modules: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'build-cache'),
]
},
module: {
rules: [
{
test: /\.ts$/,
use: [
'awesome-typescript-loader',
'angular2-template-loader'
],
exclude: [/\.(spec|e2e)\.ts$/]
},
{
test: /\.html$/,
use: 'raw-loader'
},
{
test: /\.css$/,
use: [
'to-string-loader',
'css-loader'
]
},
]
},
plugins: [
new StatsPlugin('stats.json', 'verbose'),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({
beautify: false,
output: {
comments: false
},
mangle: {
screw_ie8: true,
except: ['$']
},
compress: {
screw_ie8: true,
warnings: false,
collapse_vars: true,
reduce_vars: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true,
drop_console: false,
drop_debugger: true
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/polyfills',
chunks: ['shared/polyfills']
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/vendors',
chunks: [
'app1/app',
'app2/app',
'app3/app'
],
// Move all node_modules into vendors chunk but not @angular
minChunks: module => /node_modules\/(?!@angular)/.test(module.resource)
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/angular-vendors',
chunks: [
'app1/app',
'app2/app',
'app3/app'
],
// Move all node_modules into vendors chunk but not @angular
minChunks: module => /node_modules\/@angular/.test(module.resource)
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'shared/shared-modules',
chunks: [
'app1/app',
'app2/app',
'app3/app'
],
// Move duplicate modules to a shared-modules chunk
minChunks: 2
}),
// Specify the correct order the scripts will be injected in
new webpack.optimize.CommonsChunkPlugin({
name: [
'shared/polyfills',
'shared/vendors',
'shared/angular-vendors'
].reverse()
}),
new HtmlWebpackPlugin({
template: testHTML,
inject: false
})
],
node: {
global: true,
crypto: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: false
},
target: 'web'
}
After running any one of the commands given above, two new folders named build-cache
and dist
(these two folders are mentioned in the webpack config files) have been created in the root application folder directory, as shown below.
The dist folder is the output folder where the final files have been stored after compilation. If we use JIT compilation, then the output files will be stored under the dev folder, where the same will be stored under the prod folder for the AOT compilations. After taht, we just need to deploy the contents of these folders (either dev or prod) to the server as an application. Also, note that this compiled folder does not contains any node_modules
folder because it has already bundled the related content of the required node module package files within the shared-modules.js
and polyfill.js
files.
You can download the complete source code from the link - Angular AOT Compilation
Opinions expressed by DZone contributors are their own.
Comments