Wenn du in Eile bist, sei faul

Die Angular-Abenteuer: Optimieren mit Lazy Loading

Manfred Steyer

© SuS Media

In der Kolumne Die Angular-Abenteuer stürzt sich Manfred Steyer mitten ins Getümmel der dynamischen Welt der Webentwicklung. Tipps und Tricks für Angular-Entwickler kommen dabei ebenso zur Sprache wie aktuelle Entwicklungen im JavaScript-Ökosystem.
 

Dank Lazy Loading muss eine Angular-Anwendung ihre Module erst bei Bedarf laden. Das verbessert die Startgeschwindigkeit. Zum Kompensieren des dadurch entstehenden Mehraufwands zur Laufzeit kümmert sich Preloading im Hintergrund ums Laden dieser Module nach dem Programmstart.

Um die Performance einer Anwendung zu optimieren, gibt es ein paar Quick Wins. Darunter fällt Bundling, Minification, aber auch das Aktvieren des Produktionsmodus von Angular. Die gute Nachricht: Das Angular CLI kümmert sich darum und um noch viel mehr automatisch, wenn es einen Production Build erzeugt.Um diesen zu bekommen, genügt ein einfaches ng build –prod.

Daneben bringt Angular noch ein paar weitere Hebel zur Verbesserung der Performance mit. Einen, der sich bei großen Unternehmensanwendungen bewährt hat, möchte ich heute vorstellen. Die Rede ist von Lazy Loading. Wie immer stelle ich das zur Veranschaulichung genutzte Beispiel über mein GitHub-Konto zur Verfügung.

Lazy Loading konfigurieren

Standardmäßig lädt der Browser alle Angular-Module, bevor der Benutzer die Arbeit damit aufnehmen kann. Darunter befinden sich natürlich auch einige, die nicht oder zumindest nicht sofort benötigt werden. Genau hier setzt Lazy Loading zur Optimierung der Startgeschwindigkeit an: Es sorgt dafür, dass zunächst nur die wichtigsten Anwendungsbestandteile im Browser landen, und der Rest wird später bei Bedarf angefordert. Hierfür kommen einfach Routen mit der Eigenschaft loadChildren zum Einsatz (Listing 1).

let APP_ROUTES: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'flight-booking',
    loadChildren: './flight-booking/flight-booking.module#FlightBookingModule'
  },
  […]
}

Die Eigenschaft loadChildren verweist auf einen String, der zwei durch eine Raute getrennte Informationen erhält. Der erste Teil benennt die Datei, in der sich das bei Bedarf zu ladende Modul befindet, der zweite Teil gibt Auskunft über den Namen der Modulklasse.

Damit Lazy Loading funktioniert, ist darauf zu achten, dass kein anderer Teil der Anwendung das FlightBookingModul direkt referenziert. Die meisten Build-Tools würden solchen Referenzen nämlich folgen und das so entdeckte Modul ins aktuelle Bundle aufnehmen. Wenn aber alles im selben Bundle landet, wird die Idee von Lazy Loading ad absurdum geführt. Aus diesem Grund darf selbst das AppModule die mit loadChildren angegebenen Module nicht mehr importieren (Listing 2).

@NgModule({
  imports: [
    BrowserModule,
    HttpModule,
    FormsModule,
    […],
  // FlightBookingModule // <-- Würde Lazy Loading verhindern
  ],
  […]
})
export class AppModule {
}

Darüber hinaus ist beim Einsatz von Shared Modules zu beachten, dass diese keine öffentlichen Provider anbieten. Da ein verzögertes Modul seinen eigenen Dependency Injection Scope bekommt, könnte das dazu führen, dass Services mehrfach instanziiert werden, obwohl sie als Singletons agieren sollten.

Lazy Loading nachvollziehen

Hat man alles richtig gemacht, splittet das Angular CLI ein eigenes Bundle für das konfigurierte Modul ab (Abb. 1).

Abb. 1: CLI splittet Modul ab

Dazu nutzt das CLI das npm-Paket @ngtools/webpack. Es bietet ein webpack-Plug-in sowie einen webpack-Loader an, die sich unter anderem um das Kompilieren und Splitten der Bundles kümmern. Wer direkt auf webpack setzt, kann dieses Paket auch selbst in der webpack-Konfiguration nutzen.

Ob das konfigurierte Lazy Loading wirklich funktioniert, lässt sich einfach im Browser nachvollziehen. Hierzu erweist sich beispielsweise das Registerblatt NETWORK in den Dev-Tools von Chrome als nützlich (Abb. 2). Es zeigt, wann Chrome welche Dateien lädt. Funktioniert Lazy Loading, lädt Angular das abgespaltene Bundle erst, wenn es benötigt wird.

Abb. 2: Lazy Loading im Browser nachvollziehen

Preloading

Lazy Loading verbessert zwar die Startgeschwindigkeit, aber das hat einen Haken: Die konfigurierten Module müssen zur Laufzeit geladen werden, das verzögert die Ausführung. Der Benutzer muss nach einem Klick also gegebenenfalls erst einmal warten, bis das Bundle der Wahl im Browser gelandet ist.

Genau hier setzt Preloading an. Es sieht vor, dass nach dem Start der Anwendung die verzögerten Bundles im Hintergrund geladen werden. Somit ergibt sich dank Lazy Loading eine schnelle Startgeschwindigkeit, und mit etwas Glück ist das gewünschte Bundle auch schon da, wenn der Benutzer ins jeweilige Modul navigiert.

Zum Aktivieren von Preloading ist lediglich beim Einrichten der Routen für das Hauptmodul (auch Root Module oder App Module genannt) eine PreloadingStrategy anzugeben:

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

[...]

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

Die hier verwendete Strategie PreloadAllModules führt dazu, dass Angular beim Programmstart sämtliche Module per Preloading bezieht. Das Ergebnis dieses Unterfangens lässt sich in Chrome mit dem Registerblatt NETWORK in den Dev-Tools beobachten. Da es sehr schnell gehen kann, lokale Dateien zu laden, empfiehlt es sich, dabei die Netzwerkgeschwindigkeit zu drosseln. Abbildung 3 demonstriert beispielsweise das Ladeverhalten bei einer simulierten langsamen 3G-Verbindung.

Abb. 3: Preloading im Browser nachvollziehen

Beim Laden der Seite zeigt das betrachtete Fenster, dass Angular das Bundle mit dem FlightBookingModule erst nach dem Start der Anwendung lädt. Da dieses Bundle jedoch recht klein ist, muss man dazu sehr genau schauen. Deswegen werfen wir einen Blick auf ein Experiment, mit dem sich dieser Umstand besser nachvollziehen lässt.

Preloading mit einem Experiment nachvollziehen

Um zu begreifen, dass das Preloading erst nach dem Start der Anwendung beginnt, kommt 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 der Typ PreloadingStrategy zu implementieren:

// custom-preloading-strategy.ts
import {PreloadingStrategy, Route} from "@angular/router";
import {Observable} from 'rxjs/Observable';

export class CustomPreloadingStrategy implements PreloadingStrategy {
    preload(route: Route, fn: () => Observable): Observable {
        return Observable.of(true).delay(7000).flatMap(_ => fn());
    }
}

Die Methode preload der PreloadingStrategy erhält von Angular die Route, die es zu laden gilt, sowie eine Funktion, die das Laden übernimmt. Sie kann entscheiden, ob die betroffene Route per Preloading bezogen werden soll, und diesen Vorgang gegebenenfalls 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 ihn mit einer Verzögerung von sieben Sekunden. Nach dieser Zeitspanne führt flatMap das Preloading durch.

Um die CustomPreloadingStrategy zu verwenden, ist darauf beim Erzeugen des konfigurierten AppRoutesModule zu verweisen:

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

Außerdem ist die CustomPreloadingStrategy als Provider zu registrieren. Das Fenster Network in den Dev-Tools zeigt nun sehr deutlich, dass die Anwendung wie gewünscht erst nach dem Programmstart mit ungenutzten Ressourcen und Preloading das Modul lädt (Abb. 4).

Abb. 4: Eigene Preloading-Strategie

Selektives Preloading mit eigener Preloading-Strategie

Nachdem das letzte Experiment gezeigt hat, wie sich eigene Preloading-Strategien entwickeln lassen, soll hier nun eine sinnvolle vorgestellt werden. Das Ziel ist es, nur bestimmte Routen vorzuladen. Dazu erhalten die gewünschten Routen zunächst eine benutzerdefinierte Eigenschaft preload (Listing 3)

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'
  }
];

Für solche benutzerdefinierten Erweiterungen ist die Eigenschaft data in den Routen vorgesehen. Sie ist vom Typ any und kann somit alle beliebigen Informationen aufnehmen. Die Preloading-Strategie prüft nun, ob die übergebene Route diese Eigenschaft aufweist (Listing 4).

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

export class CustomPreloadingStrategy implements PreloadingStrategy {

  preload(route: Route, fn: () => Observable): Observable {
    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 zurück, das den Wert null transportiert.

Zusammenfassung

Lazy Loading ist eine effektive Lösung, um die Startgeschwindigkeit einer Angular-Anwendung zu verbessern. Dazu kommt der angenehme Nebeneffekt, dass jedes verzögerte Modul ein eigenes URL-Segment bekommt. Sieht man diese Lösung von Anfang an vor, ist sie auch recht einfach umzusetzen: Ein entsprechender Routeneintrag pro Modul genügt.

Da Lazy Loading jedoch die Geschwindigkeit zur Laufzeit ein wenig verschlechtert, bietet sich zusätzlich der Einsatz von Preloading an. Wer das Vorladen der Module nach Programmstart kontrollieren möchte, kann auch eine eigene PreloadingStrategy einklinken.

Geschrieben von
Manfred Steyer
Manfred Steyer
Manfred Steyer ist selbstständiger Trainer und Berater mit Fokus auf Angular 2. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google. Web: www.softwarearchitekt.at
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: