Getting Started with Route Preloading

Angular's modern Router introduces lazy loading of feature modules, which dramatically improves initial bootstrap times for single-page applications. At AngularConnect 2016 in London, core team member Victor Savkin took this concept further by demonstrating a strategy for preloading modules during idle browser time. Instead of waiting for a user to navigate to a route before downloading its module, the application proactively fetches lazy-loaded modules after startup, making them instantly available when needed.

This guide walks through implementing module preloading in an Angular project. The complete working example is hosted on GitHub, and it relies on Angular 2.1.0-beta.0 alongside Router 3.1.0-beta.0—the first releases to include this capability.

Baseline Setup

The demonstration app centers on an AppModule that lazily loads a FlightModule. The routing configuration references the module's name and its source file via the loadChildren directive:

// 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);

In this snippet, the terminal statement establishes a configured RouterModule assigned to the AppRoutesModule variable. The root-level AppModule then imports this configured module.

// 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 { 
}

To ensure loadChildren works smoothly with webpack 2, the project employs the angular2-router-loader. This package installs via npm (npm i angular2-router-loader --save-dev) and acts as an additional loader for TypeScript files in the webpack configuration:

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

The ?loader=system parameter instructs the loader to fetch lazy-loaded modules through System.import.

Enabling Preloading

Activating preloading in Router 3.1.0 and above requires only a single addition: passing a PreloadingStrategy when you create the configured AppRoutesModule.

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

[...]

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

The PreloadAllModules strategy, used here, triggers automatic prefetching of every module immediately after the application becomes operational.

Observing this behavior is straightforward with browser developer tools (F12) on the network tab. Since local files load almost instantaneously, simulating a slower network connection helps visualize the process. For instance, the screenshot below captures the loading sequence under a simulated 3G connection:

Loading behavior after activating preloading

The network panel shows the bundle 0.js—containing the FlightModule—remains unrequested until after the app has fully launched. This bundle is relatively small, so catching it in the act requires careful attention. The next section presents a more visible experiment to verify the behavior.

A Practical Preloading Experiment

For a clearer view of when preloading begins, this section introduces a custom PreloadingStrategy. This strategy intentionally introduces a several-second pause before it initiates any module loading.

Creating a bespoke strategy involves implementing the PreloadingStrategy interface, like so:

// 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());
    }

}

Angular supplies two arguments to the preload method on the interface: the target route and a function responsible for performing the actual load. This gives the strategy complete control over which routes get preloaded. The returned Observable signals to Angular when preloading for that route has completed.

The implementation shown generates an Observable holding the placeholder value true, then emits it after a 7-second delay. Following that emission, flatMap triggers the module loading.

To activate CustomPreloadingStrategy, the AppRoutesModule must reference it. Since the strategy serves purely as an injection token here, Angular also requires a provider registration for it:

// app.routes.ts

[...]

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

The AppModule must add this provider to its own providers array for the application to access it. The configured AppRoutesModule remains part of the imports:

// 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 { 
}

Now the network tab offers unmistakable evidence: the module isn't preloaded until after the application initializes.

Loading Behavior with custom preloading strategy

Targeted Preloading via Custom Strategies

Victor Savkin's AngularConnect talk also addressed restricting preloading to selected modules only. He suggested marking specific routes for preloading using a custom property named 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 data field exists precisely for such user-defined extensions. Within the PreloadingStrategy, you can examine whether the incoming route carries this preload property and if it evaluates to 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);
        }
    }

}

When a route qualifies for preloading, the strategy invokes the provided loading function and forwards the resulting Observable. For routes that don't meet the criteria, it returns a dummy Observable that emits null instead.

From here, the CustomPreloadingStrategy gets registered in the application parameters exactly as described earlier.