Webpack Tutorial: Understanding @ngtools/webpack


ag-Grid | ngtools/Webpack

AOT is a big part of using Angular. I’ve used Angular CLI successfully here at ag-Grid to build applications with AOT and found it very easy to use, but sometimes you need more control and fine tuning, and sometimes you just need an alternative, so in this blog I go through how to use ngtools/webpack, as well as describing the benefits in generally of using Webpack & AOT.

It might be useful to go through an earlier blog I wrote on Understanding Webpack for a tutorial on Webpack core concepts and ideas. This blog does go into some detail and assumes a certain level of Webpack understanding, especially in the latter half.

Introduction to @ngtools/webpack

Webpack is a module bundler but through the use of loaders and plugins we can get it to do far more, including transpiling TypeScript, processing CSS & images and now, with the inclusion of ngtools/webpack we can get it to transpile our code to make it AOT ready, all within our Webpack configuration.

All code for the blog can be found at the Webpack Tutorial: Using ngTools/webpack repository on GitHub.

Example Angular Application Code

In order to focus on the build side of things our application is deliberately simple. All the file sizes and load times are small given our applications simplicity, so it’s worth noting the relative differences in size and load times, not the absolute values.

We have a Module, a single Component and a Bootstrap file:

// boot.ts 
import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";
import {enableProdMode} from "@angular/core";
import {AppModule} from "./app.module";
declare var process;
if (process.env.ENV === 'production') {
    enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

// app.module.ts
import {NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
// application
import {AppComponent} from "./app.component";
@NgModule({
    imports: [
        BrowserModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}

// app.component.ts
import {Component} from "@angular/core";
@Component({
    selector: 'my-app',
    template: `
        Hello {{ name }}
    `
})
export class AppComponent {
    name: string = "Sean";
    constructor() {
    }
}

As you can see there’s not much to our application — no CSS or images and very little logic. This is deliberate so we can concentrate on the changes in application logic size and load times, which is what AOT really brings to the table.

The tsconfig.json for both the JIT and non-AOT configuration would be:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false,
    "lib": ["dom","es2015"]
  },
  "compileOnSave": true,
  "exclude": [
    "node_modules/*",
    "app/boot-aot.ts"
  ]
}

Not much to say about this configuration — pretty standard setup here. Note we’re excluding app/boot-aot.ts - more on that later.

The end result of running this would be:

Exciting! But the output of the application isn’t what we’re interested here — we’re interested in network traffic and load times. Let’s look at that next.

Webpack Configuration Options

In this blog I’ll run through 3 configurations:

  • JIT (Just-In-Time) Configuration: I'll describe this briefly as well as the results of using it.
  • Non-AOT Production Configuration: A Production ready configuration, but without AOT support.
  • AOT Production Configuration: A Production ready configuration, this time with AOT support.

The bulk of the blog will focus on the latter two configurations — how they compare and how they’re used.

JIT (Just-In-Time) Configuration


var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var helpers = require('./helpers');
var path = require('path');
module.exports = {
    devtool: 'cheap-module-eval-source-map',
    entry: {
        'polyfills': './app/polyfills.ts',
        'vendor': './app/vendor.ts',
        'app': './app/boot.ts'
    },
    output: {
        path: helpers.root('dist'),
        publicPath: 'http://localhost:8080/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    module: {
        loaders: [
            {
                test: /\.ts$/,
                exclude: path.resolve(__dirname, "node_modules"),
                loaders: ['awesome-typescript-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html-loader',
                query: {
                    minimize: false // workaround for ng2
                }
            }
        ]
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['app', 'vendor', 'polyfills']
        }),
        new HtmlWebpackPlugin({
            template: 'config/index.html'
        })
    ],
    devServer: {
        historyApiFallback: true,
        stats: 'minimal'
    },
};

The items of note in this configuration is the entry field where we describe our entry points, and our loaders:

  • ['awesome-typescript-loader', 'angular2-template-loader']: Transpile and process our Angular code.
  • html-loader: Take the results of our Webpack configuration and inject them into a pre-supplied html file.

Let’s run this and look at the resulting network traffic and load times, which is what we’re interested in today:

10.7MB and 1.06s to load — big, but not surprising given we’re not compressing anything and we’ve not minified anything so far.

Let’s use this as a baseline — we won’t make any further changes to this configuration — it’ll do for development purposes.

Non-AOT Production Configuration

This time let’s look at a simple configuration that will build & bundle our application — as a first pass we’ll skip minification:


const path = require('path');
const webpack = require('webpack');
const helpers = require('./helpers');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = {
    entry: {
        polyfills: './app/polyfills.ts',
        vendor: './app/vendor.ts',
        app: './app/boot.ts'
    },
    output: {
        path: helpers.root('dist/non-aot'),
        publicPath: '/',
        filename: '[name].bundle.js',
        chunkFilename: '[id].chunk.js'
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    module: {
        loaders: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html-loader'
            }
        ]
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['app', 'vendor', 'polyfills']
        }),
        new HtmlWebpackPlugin({
            template: 'config/index.html'
        }),
        new webpack.DefinePlugin({
            'process.env': {
                'ENV': JSON.stringify(ENV)
            }
        })
    ]
};

Using this we’ll get the following output:

4.3MB and 700ms to load — already a big improvement! But we can go further — let’s add minification to our setup:


plugins: [
    new webpack.optimize.CommonsChunkPlugin({
        name: ['app', 'vendor', 'polyfills']
    }),
    new HtmlWebpackPlugin({
        template: 'config/index.html'
    }),
    // minifies our code
    new webpack.optimize.UglifyJsPlugin({
        beautify: false,
        comments: false,
        compress: {
            screw_ie8: true,
            warnings: false
        },
        mangle: {
            keep_fnames: true,
            screw_i8: true
        }
    }),
    new webpack.DefinePlugin({
        'process.env': {
            'ENV': JSON.stringify(ENV)
        }
    })
]

1.3MB and 686ms to load. With very little configuration we’ve got a pretty respectable build config working here.

There is obviously one part we’re missing in our Angular application. We’re including the Angular Compiler as part of our application and are compiling our Components at runtime. Let's do something about this and see what it gets us next.

AOT Production Configuration

Similar the the Non-AOT version above, this time we reference an AOT ready bootstrap file to make use of the generated AOT factory (generated by the webpack config to follow):

// boot-aot.ts
import {platformBrowser} from "@angular/platform-browser";
import {AppModuleNgFactory} from "../aot/app/app.module.ngfactory";
import { enableProdMode } from '@angular/core';
declare var process;
if (process.env.ENV === 'production') {
    console.log("PROD MODE");
    enableProdMode();
}
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

In this bootstrap file we no longer dynamically compile our module and components, but rather use the pre-compiled factory to do it for us. This will mean much faster load times.

Next we can drop import '@angular/platform-browser-dynamic' as we won't need it (as we're not dynamically compiling anything anymore). Let's create a new vendor-aot.ts file that only includes what we need:

// Angular
import '@angular/platform-browser';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
import '@angular/forms';
// RxJS
import 'rxjs';

We need an tsconfig.json suitable for AOT transpiling too:

The tsconfig-aot.json for both the JIT and non-AOT configuration would be:


{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false,
    "lib": ["dom","es2015"]
  },
  "compileOnSave": true,
  "exclude": [
    "node_modules/*",
    "aot/",
    "app/boot-aot.ts"
  ],
  "angularCompilerOptions": {
    "genDir": "aot/",
    "skipMetadataEmit": true
  }
}

The key parts of this are:

  • exclude: We're excluding the aot output folder and boot-aot.ts AOT bootstrap file. We exclude the bootstrap file as the factory referenced within won't exist yet (the AOT plugin will do this for us, next)
  • angularCompilerOptions: Specifies AOT compiler properties, specifically the output dir here

Final Configuration with Minification

And finally, here is our AOT ready Webpack configuration (with minification included):


const path = require('path');
const webpack = require('webpack');
const helpers = require('./helpers');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AotPlugin = require('@ngtools/webpack').AotPlugin;
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = {
    entry: {
        polyfills: './app/polyfills.ts',
        vendor: './app/vendor-aot.ts',
        app: './app/boot-aot.ts'
    },
    output: {
        path: helpers.root('dist/aot'),
        publicPath: '/',
        filename: '[name].bundle.js',
        chunkFilename: '[id].chunk.js'
    },
    resolve: {
        extensions: ['.js', '.ts']
    },
    module: {
        loaders: [
            {
                test: /\.ts$/,
                loader: '@ngtools/webpack'
            },
            {
                test: /\.html$/,
                loader: 'html-loader'
            }
        ]
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['app', 'vendor', 'polyfills']
        }),
        // AOT Plugin 
        new AotPlugin({
            tsConfigPath: './tsconfig.aot.json',
            entryModule: helpers.root('app/app.module#AppModule')
        }),
        new HtmlWebpackPlugin({
            template: 'config/index.html'
        }),
        new webpack.optimize.UglifyJsPlugin({
            beautify: false,
            comments: false,
            compress: {
                screw_ie8: true,
                warnings: false
            },
            mangle: {
                keep_fnames: true,
                screw_i8: true
            }
        }),
        new webpack.DefinePlugin({
            'process.env': {
                'ENV': JSON.stringify(ENV)
            }
        })
    ]
};

The main differences here is that we now point to our new vendor-aot.ts and boot-aot.ts in our entry points, and we also make use of the ngtools/webpack AOT plugin:

new AotPlugin({
    tsConfigPath: './tsconfig.aot.json',
    entryModule: helpers.root('app/app.module#AppModule')
}),

The plugin makes use of our AOT friendly tsconfig and additionally points to the application entry point — in this case our main Application Module (app.module.ts).

Results of Optimised Webpack Configuration

The output of running this configuration would be:

951kb and 549ms to load. What a huge improvement in file size — thats almost all due to us no longer including the Angular Compiler in our application anymore.

The load time is improved too, and that’s due to our components being pre-compiled — the compilation step has been pushed to build time, which is where it belongs for production deployments.

Still though, this isn’t a huge improvement in load times and this can be explained by the fact our application is so simple — there is only one component here, so although faster already if we had more components the improvements would be even better between AOT and non-AOT builds. Let’s confirm that by adding a few simple components to our application:

app.component.ts:


import {Component} from "@angular/core";
@Component({
    selector: 'my-app',
    template: `
        Hello {{ name }}
        <hr/>
        <squared-value [value]="4"></squared-value>
        <cubed-value [value]="4"></cubed-value>
        <quadrupal-value [value]="4"></quadrupal-value>
        <hr>
        <simple-1-value></simple-1-value>
        <simple-2-value></simple-2-value>
        <simple-3-value></simple-3-value>
        <simple-4-value></simple-4-value>
        <simple-5-value></simple-5-value>
        <simple-6-value></simple-6-value>
    `
})
export class AppComponent {
    name: string = "Sean";
    constructor() {
    }
}

boot.ts:

import {platformBrowserDynamic} from "@angular/platform-browser-dynamic";
import {enableProdMode} from "@angular/core";
import {AppModule} from "./app.module";
declare var process;
if (process.env.ENV === 'production') {
    enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

// app.module.ts
import {NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
// application
import {AppComponent} from "./app.component";
import {SquareComponent} from "./components/square.component";
import {CubeComponent} from "./components/cube.component";
import {QuadComponent} from "./components/quad.component";
@NgModule({
    imports: [
        BrowserModule
    ],
    declarations: [
        AppComponent,
        SquareComponent,
        CubeComponent,
        QuadComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}

cube.component.ts:

import {Component, Input} from '@angular/core';
@Component({
    selector: 'cubed-value',
    template: `{{valueCubed()}}`
})
export class CubeComponent  {
    @Input() value: number;
    public valueCubed(): number {
        return this.value * this.value * this.value;
    }
}

quad.component.ts:

import {Component, Input} from '@angular/core';
@Component({
    selector: 'quadrupal-value',
    template: `{{valueQuadrupaled()}}`
})
export class QuadComponent  {
    @Input() value: number;
    public valueQuadrupaled(): number {
        return this.value * this.value * this.value * this.value;
    }
}

simple-1.component.ts:

// ...repeated 6 times
import {Component, Input} from '@angular/core';
@Component({
    selector: 'simple-1-value',
    template: `{{ value }}`
})
export class Simple1Component {
    @Input() value: number = 1;
}

square.component.ts:

import {Component, Input} from '@angular/core';
@Component({
    selector: 'squared-value',
    template: `{{valueSquared()}}`
})
export class SquareComponent  {
    @Input() value: number;
    public valueSquared(): number {
        return this.value * this.value;
    }
}

The output of the application now looks like this:

Still simple, we just have a few more components this time around.

The results of running this with the non-AOT configuration would be:

1.3MB and 734ms to load. The increased load time would be down to Angular needing to compile there (admittedly simple) components at runtime.

And running it with the AOT configuration would give us:

989kb and 535ms to load. Same size and even quicker (although that’s just lucky — if we ran it again it might be slower next time around)!.

Although all these values are small, especially the load times, you can hopefully see how these improvements would be magnified in a real-world application with many complex components. For very little effort you gain all the benefits of AOT while still being able to leverage the flexibility that Webpack offers. I can’t see a good reason why you wouldn’t use the two together!

Although the file sizes should be pretty consistent if you run this code, the load times can vary dramatically depending on your machine configuration and what the load on your machine is at the time. It’s probably worth looking at the load times on average in a real-world application, rather than a particular page load to get an idea of the real benefits on offer with AOT.

For a slightly more real-world example I’ve taken a number of our ag-Grid Angular Examples and placed them on a single page to see what improvements I could see. I’ve excluded file sizes from this as our examples include a number of non-compressible images, but looking at the load times I saw the following improvements:

  • Non-AOT: 2.2s
  • AOT: 1.64s

That’s a pretty decent improvement by the inclusion of a single plugin — and thats with relatively simple components. If you scale up to a full application with many components and for example lazy loading, the improvement you would see would be even greater.

For guidance on using @ngtoools/webpack with ag-Grid please see the ag-Grid @ngtools/webpack Documentation