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:
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:
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.