Optimizing Performance

With Preloading And The New Angular Router [English Version]

The new Router for Angular allows for lazy loading of modules. This way, the startup performance of an Angular based SPA can be optimized. At AngluarConnect 2016 in London, the Angluar mastermind Victor Savkin presented a preloading approach which goes beyond this: It uses free resources after the start of the application to load modules that might be requested via lazy loading later. If the router actually needs those modules later, they are available immediately.

In this post I'm showing how to use Preloading in an Angular application. The whole sample can be found here. It bases upon Angular 2.1.0-beta.0 and the Router 3.1.0-beta.0. Those are the first versions that offer this feature.

Initial Situation

The sample below uses an AppModule, that imports a FlightModule via lazy loading. For this, it references the name of the module and the file that contains it with loadChildren:

// app.routes.ts

import {Routes, RouterModule} from '@angular/router';
import {HomeComponent} from "./modules/home/home/home.component";

const ROUTE_CONFIG: Routes = [
    {
        path: 'home',
        component: HomeComponent
    },
    {
        path: 'flight-booking',
        loadChildren: './modules/flights/flight.module#FlightModule'
    },
    {
        path: '**',
        redirectTo: 'home'
    }
];

export const AppRoutesModule = RouterModule.forRoot(ROUTE_CONFIG);

Using the routing configuration, the terminal line in this listing creates a configured RouterModule which is exported via the variable AppRoutesModule. The AppModule, that serves as Root-Module, imports it.

// app.module.ts

import {NgModule} from "@angular/core";
import {AppRoutesModule} from "./app.routes";

[...]

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        AppRoutesModule,
        [...]
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [
        AppComponent 
    ]
})
export class AppModule { 
}

In order to make loadChildren play together nicely with webpack 2 the angular2-router-loader is used. It can be obtained with npm (npm i angular2-router-loader --save-dev) and serves as additional loader for .ts-files in webpack.config.js:

[...]
    module: {
        loaders: [
            [...],
            { test: /\.html$/,  loaders: ['html-loader'] },
            { test: /\.ts$/, loaders: ['angular2-router-loader?loader=system', 'awesome-typescript-loader'], exclude: /node_modules/}
        ]
    },
[...]

The parameter ?loader=system asks the loader to load modules requested by lazy loading via System.import.

Preloading

To activate preloading starting from version 3.1.0 of the router, one has only to give a PreloadingStrategy when creating the configured AppRoutesModule:

import {Routes, RouterModule, PreloadAllModules} from '@angular/router';

[...]

export const AppRoutesModule = RouterModule.forRoot(ROUTE_CONFIG, { preloadingStrategy: PreloadAllModules });

The PreloadAllModules strategy used in this example causes the Angular application to obtain all modules by means of prefetching after the start of the program.

The result of this endeavor can be witnessed in the dev tools window (F12) within the network tab. As loading local files is very fast, it is advisable to throttle the speed of the network. the following picture demonstrates for instance the loading behavior with a simulated 3G connection:

Loading behavior after activating preloading

When loading the page, the corresponding window shows that the bundle 0.js containing the FlightModule is not loaded before the application starts. As this bundle is quite small, one has to look at this very carefully. Hence, the next section describes an experiment that allows to better reproduce this fact.

Watching preloading with an experiment

For a better traceability of the fact that preloading doesn't kick in before the start of the application, this section uses a custom PreloadingStrategy. This strategy enforces a delay of some seconds, before it begins to load the modules.

Custom preloading can be easily achieved by implementing PreloadingStrategy, e.g. as follows:

// custom-preloading-strategy.ts

import {PreloadingStrategy, Route} from "@angular/router";
import {Observable} from 'rxjs';

export class CustomPreloadingStrategy implements PreloadingStrategy {

    preload(route: Route, fn: () => Observable<any>): Observable<any> {

        return Observable.of(true).delay(7000).flatMap(_ => fn());
    }

}

The method preload of the PreloadingStrategy gets from Angular the route to load as well as a function that cares for the loading itself. Thus, it can decide whether to preload the route in question. The returned Observable informs Angular, when preload has done its task.

The here considered implementation creates an Observable with the (dummy) value true and transmits it with a delay of 7 seconds. After that, flatMap triggers the preloading.

To use the CustomPreloadingStrategy, the AppRoutesModule has to point to it. As the strategy is solely used as a token at this point, Angular needs a provider for it too:

// app.routes.ts

[...]

export const AppRoutesModule = RouterModule.forRoot(ROUTE_CONFIG, { preloadingStrategy: CustomPreloadingStrategy });
export const APP_ROUTES_MODULE_PROVIDER = [CustomPreloadingStrategy];

To make the provider available for the application, the AppModule has to reference it via its array providers. The configured AppRoutesModule is still imported:

// app.module.ts
import {AppRoutesModule, APP_ROUTES_MODULE_PROVIDER} from "./app.routes";
[...]

@NgModule({
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        AppRoutesModule,
        [...]
    ],
    declarations: [
        AppComponent
    ],
    providers: [
        [...]
        APP_ROUTES_MODULE_PROVIDER
    ],
    bootstrap: [
        AppComponent 
    ]
})
export class AppModule { 
}

The Network tab within the dev tools now shows very clearly that the application starts to preload the module not before the start of the application:

Loading Behavior with custom preloading strategy

Selective preloading with custom preloading strategy

Victor Savkin also showed at AngularConnect 2016 in London how an Angular application can restrict preloading to specific modules. To mark the routes to preload, he used a custom property preload:

// app.routes.ts

import {Routes, RouterModule} from '@angular/router';
import {HomeComponent} from "./modules/home/home/home.component";

const ROUTE_CONFIG: Routes = [
    {
        path: 'home',
        component: HomeComponent
    },
    {
        path: 'flight-booking',
        loadChildren: './modules/flights/flight.module#FlightModule',
        data: { preload: true }
    },
    {
        path: '**',
        redirectTo: 'home'
    }
];

export const AppRoutesModule = RouterModule.forRoot(ROUTE_CONFIG, { preloadingStrategy: CustomPreloadingStrategy });

export const APP_ROUTES_MODULE_PROVIDER = [CustomPreloadingStrategy];

The property data is intended for such custom extensions. The PreloadingStrategy can now check, whether the passed route has this property and whether it is truthy:

// custom-preloading-strategy.ts

import {PreloadingStrategy, Route} from "@angular/router";
import {Observable} from 'rxjs';

export class CustomPreloadingStrategy implements PreloadingStrategy {

    preload(route: Route, fn: () => Observable<any>): Observable<any> {
        if (route.data['preload']) {
            return fn();
        }
        else {
            return Observable.of(null);
        }
    }

}

If the route should be preloaded, it is loading the route with the passed function and returns the received Observable. Otherwise, it returns a (dummy) Observable which transports the value null.

After that, the registration of the CustomPreloadingStrategy takes place as mentioned above.