So verbessern Sie die Leistung Ihrer Angular-App

Veröffentlicht: 2020-04-10

Wenn es um die besten Frontend-Frameworks geht, darf Angular nicht fehlen. Es erfordert jedoch viel Anstrengung von Programmierern, es zu lernen und es klug einzusetzen. Leider besteht die Gefahr, dass Entwickler, die nicht mit Angular vertraut sind, einige seiner Funktionen auf ineffiziente Weise nutzen können.

Eines der vielen Dinge, an denen Sie als Frontend-Entwickler immer arbeiten müssen, ist die Leistung der App. Ein großer Teil meiner vergangenen Projekte konzentrierte sich auf große Unternehmensanwendungen, die ständig erweitert und weiterentwickelt werden. Frontend-Frameworks wären hier extrem hilfreich, aber es ist wichtig, sie richtig und sinnvoll einzusetzen.

Ich habe eine kurze Liste der beliebtesten Strategien und Tipps zur Leistungssteigerung zusammengestellt, die Ihnen helfen können, die Leistung Ihrer Angular-Anwendung sofort zu steigern . Bitte beachten Sie, dass alle Hinweise hier für Angular in Version 8 gelten.

ChangeDetectionStrategy und ChangeDetectorRef

Change Detection (CD) ist der Mechanismus von Angular, um Datenänderungen zu erkennen und automatisch darauf zu reagieren. Wir können die grundlegende Art von Standardänderungen des Anwendungsstatus auflisten:

  • Veranstaltungen
  • HTTP-Anfrage
  • Timer

Dies sind asynchrone Interaktionen. Die Frage ist: Wie würde Angular wissen, dass einige Interaktionen (z. B. Klick, Intervall, HTTP-Anforderung) aufgetreten sind und der Anwendungsstatus aktualisiert werden muss?

Die Antwort ist ngZone , das im Grunde ein komplexes System ist, das dazu dient, asynchrone Interaktionen zu verfolgen. Wenn alle Operationen von ngZone registriert werden, weiß Angular, wann es auf einige Änderungen reagieren muss. Aber es weiß nicht, was sich genau geändert hat, und startet den Änderungserkennungsmechanismus , der alle Komponenten in erster Tiefenreihenfolge überprüft.

Jede Komponente in der Angular-App hat ihren eigenen Änderungsdetektor, der definiert, wie sich diese Komponente verhalten soll, wenn die Änderungserkennung gestartet wurde – zum Beispiel, wenn das DOM einer Komponente neu gerendert werden muss (was ein ziemlich teurer Vorgang ist). Wenn Angular die Änderungserkennung startet, wird jede einzelne Komponente überprüft und ihre Ansicht (DOM) kann standardmäßig neu gerendert werden.

Wir können dies vermeiden, indem wir ChangeDetectionStrategy.OnPush verwenden:

 @Komponente({
  Selektor: 'foobar',
  TemplateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

Wie Sie im obigen Beispielcode sehen können, müssen wir dem Decorator der Komponente einen zusätzlichen Parameter hinzufügen. Aber wie funktioniert diese neue Strategie zur Erkennung von Änderungen wirklich?

Die Strategie teilt Angular mit, dass eine bestimmte Komponente nur von ihren @Inputs() abhängt. Außerdem verhalten sich alle Komponenten @Inputs() wie ein unveränderliches Objekt (zB wenn wir nur die Eigenschaft in @Input() eines Objekts ändern, ohne die Referenz zu ändern, wird diese Komponente nicht überprüft). Es bedeutet, dass viele unnötige Überprüfungen wegfallen und es sollte unsere App-Performance erhöhen.

Eine Komponente mit ChangeDetectionStrategy.OnPush wird nur in folgenden Fällen geprüft:

  • Die @Input()-Referenz ändert sich
  • Ein Ereignis wird in der Vorlage der Komponente oder einem ihrer untergeordneten Elemente ausgelöst
  • Observable in der Komponente löst ein Ereignis aus
  • Die CD wird manuell mit dem ChangeDetectorRef-Dienst ausgeführt
  • Async-Pipe wird in der Ansicht verwendet (die Async-Pipe markiert die Komponente, die auf Änderungen überprüft werden soll – wenn der Quellstream einen neuen Wert ausgibt, wird diese Komponente überprüft)

Wenn keiner der oben genannten Fälle eintritt, führt die Verwendung von ChangeDetectionStrategy.OnPush innerhalb einer bestimmten Komponente dazu, dass die Komponente und alle verschachtelten Komponenten nach dem CD-Start nicht überprüft werden.

Glücklicherweise haben wir immer noch die volle Kontrolle über die Reaktion auf Datenänderungen, indem wir den ChangeDetectorRef-Dienst verwenden. Wir müssen uns daran erinnern, dass wir mit ChangeDetectionStrategy.OnPush innerhalb unserer Timeouts, Anfragen, Abonnement-Callbacks die CD manuell auslösen müssen, wenn wir dies wirklich brauchen:

 Zähler = 0;

Konstruktor (private changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    this.counter += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

Wie wir oben sehen können, können wir durch Aufrufen von this.changeDetectorRef.detectChanges() innerhalb unserer Timeout -Funktion CD manuell erzwingen. Wenn der Zähler in irgendeiner Weise innerhalb der Vorlage verwendet wird, wird sein Wert aktualisiert.

Der letzte Tipp in diesem Abschnitt betrifft das dauerhafte Deaktivieren von CD für bestimmte Komponenten. Wenn wir eine statische Komponente haben und sicher sind, dass ihr Status nicht geändert werden sollte, können wir CD dauerhaft deaktivieren:

 this.changeDetectorRef.detach()

Dieser Code sollte innerhalb der Lebenszyklusmethode ngAfterViewInit() oder ngAfterViewChecked() ausgeführt werden, um sicherzustellen, dass unsere Ansicht korrekt gerendert wurde, bevor wir die Datenaktualisierung deaktivieren. Diese Komponente wird während CD nicht mehr überprüft, es sei denn, wir lösen manuell detectChanges() aus.

Funktionsaufrufe und Getter im Template

Die Verwendung von Funktionsaufrufen innerhalb von Vorlagen führt diese Funktion jedes Mal aus, wenn der Änderungsdetektor ausgeführt wird. Die gleiche Situation tritt bei Gettern auf. Wenn möglich, sollten wir versuchen, dies zu vermeiden. In den meisten Fällen müssen wir nicht bei jedem CD -Lauf irgendwelche Funktionen innerhalb des Templates der Komponente ausführen. Stattdessen können wir auch reine Rohre verwenden.

Reine Rohre

Reine Pipes sind eine Art von Pipes mit einer Ausgabe, die nur von ihrer Eingabe abhängt, ohne Nebenwirkungen. Glücklicherweise sind alle Pipes in Angular standardmäßig rein.

 @Rohr({
    Name: 'Großbuchstaben',
    rein: wahr
})

Aber warum sollten wir es vermeiden, Pipes mit pure: false zu verwenden? Die Antwort ist wieder Änderungserkennung. Nicht pure Pipes werden bei jedem CD-Lauf ausgeführt, was in den meisten Fällen nicht nötig ist und die Performance unserer App mindert. Hier ist das Beispiel der Funktion, die wir in eine reine Pipe ändern können:

 transform(value: string, limit = 60, ellipse = '...') {
  if (!Wert || Wert.Länge <= Grenze) {
    Rückgabewert;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  return `${value.substr(0, numberOfVisibleCharacters)}${ellipse}`;
}

Und sehen wir uns die Aussicht an:

 <p class="description">truncate(text, 30)</p>

Der obige Code stellt die reine Funktion dar – keine Seiteneffekte, Ausgabe nur abhängig von Eingaben. In diesem Fall können wir diese Funktion einfach durch pure Pipe ersetzen:

 @Rohr({
  name: 'truncate',
  rein: wahr
})
Exportklasse TruncatePipe implementiert PipeTransform {
  transform(value: string, limit = 60, ellipse = '...') {
    ...
  }
}

Und schließlich erhalten wir in dieser Ansicht den Code, der unabhängig von der Änderungserkennung nur dann ausgeführt wird, wenn der Text geändert wurde.

 <p class="description">{{ Text | abschneiden: 30 }}</p>

Lazy-Loading- und Preloading-Module

Wenn Ihre Anwendung mehr als eine Seite hat, sollten Sie auf jeden Fall erwägen, Module für jeden logischen Teil Ihres Projekts zu erstellen, insbesondere Lazy-Loading-Module . Betrachten wir den einfachen Angular-Router-Code:

 Konstante Routen: Routen = [
  {
    Weg: '',
    Komponente: HomeComponent
  },
  {
    Pfad: 'foo',
    loadChildren: ()=> import(./foo/foo.module").then(m => m.FooModule)
  },
  {
    Pfad: 'bar',
    loadChildren: ()=> import(./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  exports: [RouterModul],
  imports: [RouterModule.forRoot(routen)]
})
Klasse AppRoutingModule {}

Im obigen Beispiel sehen wir, dass das fooModule mit all seinen Assets nur geladen wird, wenn der Benutzer versucht, eine bestimmte Route (foo oder bar) einzugeben. Angular generiert auch einen separaten Chunk für dieses Modul . Lazy Loading reduziert den anfänglichen Ladevorgang.

Wir können noch weitere Optimierungen vornehmen. Nehmen wir an, wir möchten unsere App- Lademodule im Hintergrund ausführen. Für diesen Fall können wir die preloadingStrategy verwenden. Angular hat standardmäßig zwei Arten von preloadingStrategy:

  • Kein Vorladen
  • Alle Module vorladen

Im obigen Code wird standardmäßig die NoPreloading-Strategie verwendet. Die App beginnt, ein bestimmtes Modul auf Benutzeranfrage zu laden (wenn der Benutzer eine bestimmte Route sehen möchte). Wir können dies ändern, indem wir dem Router eine zusätzliche Konfiguration hinzufügen.

 @NgModule({
  exports: [RouterModul],
  imports: [RouterModule.forRoot(routen, {
       preloadingStrategie: PreloadAllModules
  }]
})
Klasse AppRoutingModule {}

Diese Konfiguration bewirkt, dass die aktuelle Route so schnell wie möglich angezeigt wird und die Anwendung danach versucht, die anderen Module im Hintergrund zu laden. Clever, nicht wahr? Aber das ist nicht alles. Wenn diese Lösung nicht unseren Anforderungen entspricht, können wir einfach unsere eigene benutzerdefinierte Strategie schreiben.

Nehmen wir an, wir wollen nur ausgewählte Module vorladen, zum Beispiel BarModule. Wir zeigen dies an, indem wir ein zusätzliches Feld für das Datenfeld hinzufügen.

 Konstante Routen: Routen = [
  {
    Weg: '',
    Komponente: HomeComponent
    Daten: { Vorladen: falsch }
  },
  {
    Pfad: 'foo',
    loadChildren: ()=> import(./foo/foo.module").then(m => m.FooModule),
    Daten: { Vorladen: falsch }
  },
  {
    Pfad: 'bar',
    loadChildren: ()=> import(./bar/bar.module").then(m => m.BarModule),
    Daten: { Vorladen: wahr }
  }
]

Dann müssen wir unsere benutzerdefinierte Vorladefunktion schreiben:

 @Injizierbar()
Exportklasse CustomPreloadingStrategy implementiert PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

Und legen Sie es als preloadingStrategy fest:

 @NgModule({
  exports: [RouterModul],
  imports: [RouterModule.forRoot(routen, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
Klasse AppRoutingModule {}

Vorerst werden nur Routen mit param { data: { preload: true } } vorgeladen. Der Rest der Routen verhält sich so, als wäre NoPreloading eingestellt.

Custom preloadingStrategy ist @Injectable(), das bedeutet, dass wir bei Bedarf einige Dienste einfügen und unsere preloadingStrategy auf andere Weise anpassen können.
Mit den Entwicklertools eines Browsers können wir den Leistungsschub durch gleiche anfängliche Ladezeit mit und ohne PreloadingStrategy untersuchen. Wir können auch auf der Registerkarte Netzwerk nachsehen, dass Chunks für andere Routen im Hintergrund geladen werden, während der Benutzer die aktuelle Seite ohne Verzögerung sehen kann.

trackBy-Funktion

Wir können davon ausgehen, dass die meisten Angular-Apps *ngFor verwenden, um über die in der Vorlage aufgeführten Elemente zu iterieren. Wenn die iterierte Liste auch editierbar ist, ist trackBy absolut ein Muss.

 <ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ produkt.titel }}</td>
  </tr>
</ul>

trackByProductId(Index: Nummer, Produkt: Produkt) {
  Produkt.id zurückgeben;
}

Durch die Verwendung der trackBy-Funktion ist Angular in der Lage zu verfolgen, welche Elemente von Sammlungen sich geändert haben (durch gegebene Kennung) und nur diese bestimmten Elemente neu zu rendern. Wenn wir trackBy weglassen, wird die gesamte Liste neu geladen, was eine sehr ressourcenintensive Operation auf DOM sein kann.

Ahead-of-Time (AOT)-Kompilierung

Zur Angular-Dokumentation:

(…) von Angular bereitgestellte Komponenten und Templates können vom Browser nicht direkt verstanden werden, Angular-Anwendungen erfordern einen Kompilierungsprozess, bevor sie in einem Browser ausgeführt werden können

Angular bietet zwei Arten des Kompilierens:

  • Just-in-Time (JIT) – kompiliert eine App zur Laufzeit im Browser
  • Ahead-of-Time (AOT) – kompiliert eine App zur Build-Zeit

Für die Verwendung in der Entwicklung sollte die JIT- Kompilierung die Anforderungen der Entwickler abdecken. Trotzdem sollten wir für den Produktionsaufbau unbedingt das AOT verwenden. Wir müssen sicherstellen, dass das aot- Flag in der Datei angle.json auf „true“ gesetzt ist. Zu den wichtigsten Vorteilen einer solchen Lösung gehören schnelleres Rendern, weniger asynchrone Anfragen, kleinere Framework-Downloadgröße und erhöhte Sicherheit.

Zusammenfassung

Die Leistung der Anwendung ist etwas, das Sie sowohl während der Entwicklung als auch während des Wartungsteils Ihres Projekts im Auge behalten müssen. Die Suche nach möglichen Lösungen auf eigene Faust kann jedoch sowohl zeit- als auch arbeitsaufwändig sein. Wenn Sie diese häufig gemachten Fehler überprüfen und während des Entwicklungsprozesses berücksichtigen, können Sie nicht nur die Leistung Ihrer Angular-App im Handumdrehen verbessern, sondern auch zukünftige Fehler vermeiden.

Produktsymbol freigeben

Vertrauen Sie Miquido Ihr Angular-Projekt an

Kontaktiere uns

Möchten Sie eine App mit Miquido entwickeln?

Denken Sie darüber nach, Ihrem Unternehmen mit einer Angular-App einen Schub zu geben? Nehmen Sie Kontakt mit uns auf und wählen Sie unsere Angular-App-Entwicklungsdienste aus.