Performanceoptimierung

Mit Preloading Und Dem Neuen Angular Router

Der neue Router für Angular erlaubt das verzögerte Laden (Lazy Loading) von Modulen. Auf diese Weise lässt sich die Startgeschwindigkeit einer Angular-basierten SPA optimieren. Das von Angular-Mastermind Victor Savkin auf der AngularConnect 2016 in London vorgestellte Preloading geht darüber hinaus, indem es eine weitere Performanceoptimierungen erlaubt: Es nutzt freie Ressourcen nach dem Start der Anwendung zum Nachladen von Modulen, die später per Lazy Loading angefordert werden könnten. Werden der Router diese Module später tatsächlich benötigt, stehen sie augenblicklich zur Verfügung.

In diesem Beitrag zeige ich, wie Preloading in einer Angular-Anwendung genutzt werden kann. Das gesamte Beispiel findet man hier. Es basiert auf Angular 2.1.0-beta.0 und dem Router 3.1.0-beta.0. Dabei handelt es sich um die ersten Versionen, welche dieses Feature anbieten.

Ausgangssituation

Das hier vorgestellte Beispiel nutzt ein AppModule, welches per Lazy Loading ein FlugModule einbindet. Dazu verweist es über loadChildren auf den Namen des Modules und die Datei, in dem es zu finden ist:

// 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: 'flug-buchen',
        loadChildren: './modules/flug/flug.module#FlugModule'
    },
    {
        path: '**',
        redirectTo: 'home'
    }
];

export const AppRoutesModule = RouterModule.forRoot(ROUTE_CONFIG);

Die abschließende Zeile in diesem Listing erzeugt mit der Routing-Konfiguration eine konfigurierte Variante des RouterModules und exportiet diese über die Variable AppRoutesModule. Das AppModule, welches hier als Root-Module dient, verweist darauf:

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

Damit loadChildren in der Routen-Konfiguration auf die gezeigte eingängige Weise mit Webpack 2 zusammenspielt, kommt der angular2-router-loader zum Einsatz. Dieser lässt sich mit npm beziehen (npm i angular2-router-loader --save-dev) und dient als zusätzlicher Loader für .ts-Dateien in der webpack.config.js:

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

Der Parameter ?loader=system veranlasst den Loader dazu, die per Lazy Loading angeforderten Module via System.import zu laden.

Preloading

Zum Aktivieren von Preloading ist ab Version 3.1.0 des Routers lediglich beim Erzeugen des konfigurierten AppRoutesModule eine PreloadingStrategie anzugeben:

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

[...]

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

Die hier verwendete Strategie PreloadAllModules führt dazu, dass die Angular-Anwendung dem Programmstart sämtliche Module per Preloading bezieht.

Das Ergebnis dieses Unterfangens lässt sich in Chrome in den F12-Dev-Tools unter Network beobachten. Da das Laden lokaler Dateien sehr schnell von statten geht, empfiehlt es sich, dabei die Netzwerkgeschwindigkeit zu drosseln. Die nachfolgende Abbildung demonstriert zum Beispiel das Ladeverhalten bei einer simulierten 3G-Verbindung:

Ladeverhalten nach Aktivieren von Preloading

Beim Laden der Seite zeigt das betrachtete Fenster, dass Angular das Bundle 0.js mit dem FlugModule erst nach dem Start der Anwendung lädt. Da dieses Bundle jedoch recht klein ist, muss man dazu sehr genau schauen. Deswegen beschreibt der nächste Abschnitt ein Experiment, mit dem dieser Umstand besser nachvollzogen werden kann.

Preloading mit Experiment nachvollziehen

Zum besseren Nachvollziehen der Tatsache, dass das Preloading erst nach dem Start der Anwendung beginnt, kommt in diesem Abschnitt eine benutzerdefinierte Preloading-Strategie zum Einsatz. Diese führt mit RxJS eine Verzögerung von ein paar Sekunden aus, bevor sie sich um das Laden des Modules kümmert.

Zum Bereitstellen einer eigenen Preloading-Strategie ist das Interface PreloadingStrategy zu implementieren:

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

}

Die Methode preload der PreloadingStrategy erhält Angular die Route, welche es zu laden gilt, sowie eine Funktion, die das Laden übernimmt. Somit kann sie entscheiden, ob die betroffene Route per Preloading bezogen werden soll und diesen Vorgang ggf. auch anstoßen. Das retournierte Observable informiert Angular, wenn preload ihre Aufgabe erledigt hat.

Die hier betrachtete Implementierung erzeugt ein Observable mit dem (Dummy-)Wert true und versendet diesen mit einer Verzögerung von 7 Sekunden. Nach dieser Zeitspanne führt flatMap das Preloading durch.

Um die CustomPreloadingStrategy zu verwenden, ist darauf beim Erzeugen des konfigurierte AppRoutesModule zu verweisen. Da an dieser Stelle die Strategie lediglich als Token zum Einsatz kommt, benötigt Angular zusätzlich einen Provider dafür:

// app.routes.ts

[...]

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

Damit der Provider der Anwendung zur Verfügung steht, referenziert das AppModule ihn über ihr Array providers. Das konfigurierte AppRoutesModule referenziert es natürlich nach wie vor:

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

Das Fenster Network in den Dev-Tools zeigt nun sehr deutlich, dass die Anwendung wie gewünscht erst nach dem Programmstart mit ungenützten Ressourcen und Preloading das Module lädt:

Ladeverhalten mit eigener Preloading-Strategie

Selektives Preloading mit eigener Preloading-Strategie

Victor Savkin hat auf der AngularConnect 2016 in London auch gezeigt, wie sich eine Angular-Anwendung beim Preloading auf bestimmte Module beschränken kann. Dazu erhalten die gewünschten Routen eine benutzerdefinierte Eigenschaft 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: 'flug-buchen',
        loadChildren: './modules/flug/flug.module#FlugModule',
        data: { preload: true }
    },
    {
        path: '**',
        redirectTo: 'home'
    }
];

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

export const APP_ROUTES_MODULE_PROVIDER = [CustomPreloadingStrategy];

Die Eigenschaft data ist für solche benutzerdefinierten Erweiterungen vorgesehen. Die Preloading-Strategie kann nun prüfen, ob die übergebene Route diese Eigenschaft aufweist sowie ob sie truthy ist:

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

}

In diesem Fall lädt sie die Route mit der entgegengenommenen Funktion und retourniert das von ihr erhaltene Observable. Ansonsten liefert sie ein (Dummy-)Observable, welches den Wert null transportiert, zurück.

Das Registrieren der CustomPreloadingStrategy erfolgt darauf hin wie weiter oben beschrieben.