Angular-Entwickler haben zone.js viel zu verdanken. Sie hilft zum Beispiel dabei, die Arbeit mit Angular auf magische Weise zu vereinfachen. In der Tat rendert Angular die entsprechenden Komponenten fast immer neu, wenn Sie nur eine Eigenschaft ändern müssen und wir sie ändern, ohne an irgendetwas zu denken. Infolgedessen enthält das, was der Benutzer sieht, immer die neuesten Informationen. Das ist einfach toll.
Hier möchte ich einige Aspekte untersuchen, wie die Verwendung des neuen Ivy-Compilers (der in Angular 9 erschien) die Ablehnung der Verwendung von zone.js erheblich erleichtern kann.

Durch den Verzicht auf diese Bibliothek konnte ich die Leistung der unter hoher Last ausgeführten Angular-Anwendung erheblich steigern. Gleichzeitig gelang es mir, die von mir benötigten Mechanismen mithilfe von TypeScript-Dekoratoren zu implementieren, was zu sehr geringen zusätzlichen Systemressourcen führte.
Beachten Sie, dass der in diesem Artikel beschriebene Ansatz zur Optimierung von Angular-Anwendungen nur möglich ist, weil Angular Ivy und AOT standardmäßig aktiviert sind. Dieser Artikel wurde zu Bildungszwecken verfasst und zielt nicht darauf ab, den darin vorgestellten Ansatz für die Entwicklung von Angular-Projekten zu fördern.
Warum müssen Sie möglicherweise Angular ohne zone.js verwenden?
Bevor wir fortfahren, stellen wir eine wichtige Frage: „Lohnt es sich, zone.js loszuwerden, da diese Bibliothek uns hilft, Vorlagen mit geringem Aufwand neu zu rendern?“ Natürlich ist diese Bibliothek sehr nützlich. Aber wie immer muss man für alles bezahlen.
Wenn für Ihre Anwendung bestimmte Leistungsanforderungen gelten, können Sie diese durch Deaktivieren von zone.js erfüllen. Ein Beispiel für eine Anwendung, bei der die Leistung von entscheidender Bedeutung ist, ist ein Projekt, dessen Oberfläche sehr häufig aktualisiert wird. In meinem Fall erwies sich ein solches Projekt als Echtzeit-Handelsanwendung. Sein Client-Teil empfängt ständig Nachrichten über das WebSocket-Protokoll. Die Daten aus diesen Nachrichten sollten so schnell wie möglich angezeigt werden.
Entfernen Sie zone.js von Angular
Angular kann sehr einfach zum Arbeiten ohne zone.js gebracht werden. Dazu müssen Sie zuerst den entsprechenden Importbefehl
polyfills.ts
oder löschen, der sich in der Datei
polyfills.ts
.
Auskommentierter Befehl zum Importieren von zone.jsWeiter - Sie müssen das Root-Modul mit den folgenden Optionen ausstatten:
platformBrowserDynamic() .bootstrapModule(AppModule, { ngZone: 'noop' }) .catch(err => console.error(err));
Angular Ivy: Selbsterkennende Änderungen mit ɵdetectChanges und ɵmarkDirty
Bevor wir mit dem Erstellen eines TypeScript-Dekorators beginnen können, müssen wir lernen, wie Sie mit Ivy den Prozess zum Erkennen von Komponentenänderungen, zum Verschmutzen und zum Umgehen von zone.js und DI aufrufen können.
Jetzt stehen uns zwei zusätzliche Funktionen zur Verfügung, die aus
@angular/core
exportiert werden. Dies sind
ɵdetectChanges
und
ɵmarkDirty
. Diese beiden Funktionen sind weiterhin für den internen Gebrauch bestimmt und instabil - das Symbol
ɵ
befindet sich am Anfang ihres Namens.
Schauen wir uns an, wie diese Funktionen verwendet werden.
▍ ɵmarkDirty-Funktion
Mit dieser Funktion können Sie eine Komponente als "verschmutzt" markieren, dh, sie muss neu gerendert werden. Sie plant, den Änderungserkennungsprozess zu starten, wenn die Komponente vor dem Aufrufen nicht als "verschmutzt" markiert war.
import { ɵmarkDirty as markDirty } from '@angular/core'; @Component({...}) class MyComponent { setTitle(title: string) { this.title = title; markDirty(this); } }
▍ ɵdetectChanges-Funktion
Angular interne Dokumentation besagt, dass Sie aus Leistungsgründen
ɵdetectChanges
nicht verwenden
ɵdetectChanges
. Stattdessen wird empfohlen, die
ɵmarkDirty
functionmarkDirty zu verwenden. Die Funktion
ɵdetectChanges
ruft synchron den Prozess zum Erkennen von Änderungen in einer Komponente und ihren Unterkomponenten auf.
import { ɵdetectChanges as detectChanges } from '@angular/core'; @Component({...}) class MyComponent { setTitle(title: string) { this.title = title; detectChanges(this); } }
Automatische Erkennung von Änderungen mit TypeScript Decorator
Obwohl die von Angular bereitgestellten Funktionen die Benutzerfreundlichkeit der Entwicklung erhöhen, indem die DI herumlaufen gelassen wird, kann der Programmierer dennoch frustriert sein, dass er diese Funktionen importieren und selbst aufrufen muss, um den Änderungserkennungsprozess zu starten.
Um den automatischen Start der Änderungserkennung zu vereinfachen, können Sie einen TypeScript-Dekorator schreiben, der dieses Problem unabhängig löst. Natürlich gibt es hier einige Einschränkungen, die wir unten diskutieren werden, aber in meinem Fall hat sich herausgestellt, dass dieser Ansatz genau das war, was ich brauchte.
▍Einführung des @observed Decorators
Um Änderungen mit möglichst geringem Aufwand zu erkennen, erstellen wir einen Dekorateur, der auf drei Arten angewendet werden kann. Es ist nämlich auf die folgenden Entitäten anwendbar:
- Zu synchronen Methoden.
- Beobachtbare Objekte.
- Zu gewöhnlichen Gegenständen.
Betrachten Sie ein paar kleine Beispiele. Im folgenden Codefragment wenden wir den
@observed
Dekorator auf das
changeTitle
und auf die
changeTitle
Methode an:
export class Component { title = ''; @observed() state = { name: '' }; @observed() changeTitle(title: string) { this.title = title; } changeName(name: string) { this.state.name = name; } }
- Um nach Änderungen am
state
suchen, verwenden wir ein Proxy-Objekt, das Änderungen am Objekt abfängt und die Prozedur zum Erkennen von Änderungen aufruft. - Wir überschreiben die
changeTitle
Methode, indem wir eine Funktion anwenden, die diese Methode zuerst aufruft und dann den Änderungserkennungsprozess startet.
Und hier ist ein Beispiel mit
BehaviorSubject
:
export class AppComponent { @observed() show$ = new BehaviorSubject(true); toggle() { this.show$.next(!this.show$.value); } }
Bei beobachtbaren Objekten sieht die Verwendung eines Dekorateurs etwas komplizierter aus. Sie müssen das beobachtete Objekt abonnieren und die Komponente im Abonnement als "unsauber" markieren, aber Sie müssen auch das Abonnement löschen. Zu diesem
ngOnInit
ngOnDestroy
wir
ngOnInit
und
ngOnDestroy
zu, um sie später zu abonnieren und zu bereinigen.
▍Erstellen eines Dekorateurs
Hier ist die Unterschrift des Dekorateurs:
export function observed() { return function( target: object, propertyKey: string, descriptor?: PropertyDescriptor ) {} }
Wie Sie sehen, ist der
descriptor
ein optionaler Parameter. Dies liegt daran, dass der Dekorator sowohl auf Methoden als auch auf Eigenschaften angewendet werden muss. Wenn der Parameter vorhanden ist, bedeutet dies, dass der Dekorator auf die Methode angewendet wird. In diesem Fall machen wir das:
Als nächstes müssen Sie prüfen, mit welcher Art von Immobilie wir es zu tun haben. Es kann ein beobachtbares Objekt oder ein gewöhnliches Objekt sein. Hier werden wir eine andere interne Angular-API verwenden. Es ist meines Erachtens nicht zur Verwendung in regulären Anwendungen vorgesehen (sorry!).
Es handelt sich um die
ɵcmp
Eigenschaft, mit der auf die von Angular verarbeiteten Eigenschaften
ɵcmp
, nachdem sie definiert wurden. Wir können sie verwenden, um die Methoden der
onDestroy
onInit
und
onDestroy
zu überschreiben.
const getCmp = type => (type).ɵcmp; const cmp = getCmp(target.constructor); const onInit = cmp.onInit || noop; const onDestroy = cmp.onDestroy || noop;
Um eine Eigenschaft als zu überwachend zu kennzeichnen, verwenden wir
ReflectMetadata
und setzen ihren Wert auf
true
. Als Ergebnis wissen wir, dass wir die Eigenschaft beim Initialisieren der Komponente beobachten müssen:
Reflect.set(target, propertyKey, true);
Jetzt ist es Zeit, den
onInit
Hook zu überschreiben und die Eigenschaften beim Erstellen der Komponenteninstanz zu überprüfen:
cmp.onInit = function() { checkComponentProperties(this); onInit.call(this); };
Wir definieren die Funktion
checkComponentProperties
, die die Eigenschaften der Komponente
Reflect.set
sie nach dem zuvor mit
Reflect.set
festgelegten Wert
Reflect.set
:
const checkComponentProperties = (ctx) => { const props = Object.getOwnPropertyNames(ctx); props.map((prop) => { return Reflect.get(target, prop); }).filter(Boolean).forEach(() => { checkProperty.call(ctx, propertyKey); }); };
Die
checkProperty
Funktion ist für die Dekoration einzelner Eigenschaften verantwortlich. Zuerst prüfen wir, ob es sich bei der Eigenschaft um ein Observable oder ein reguläres Objekt handelt. Wenn es sich um ein beobachtbares Objekt handelt, abonnieren wir es und fügen das Abonnement der Liste der Abonnements hinzu, die in der Komponente für ihre internen Anforderungen gespeichert sind.
const checkProperty = function(name: string) { const ctx = this; if (ctx[name] instanceof Observable) { const subscriptions = getSubscriptions(ctx); subscriptions.add(ctx[name].subscribe(() => { markDirty(ctx); })); } else {
Wenn es sich bei der Eigenschaft um ein
markDirty
Objekt handelt, konvertieren wir es in ein Proxy-Objekt und rufen in seiner
markDirty
auf:
const handler = { set(obj, prop, value) { obj[prop] = value; ɵmarkDirty(ctx); return true; } }; ctx[name] = new Proxy(ctx, handler);
Schließlich müssen Sie das Abonnement löschen, nachdem Sie die Komponente zerstört haben:
cmp.onDestroy = function() { const ctx = this; if (ctx[subscriptionsSymbol]) { ctx[subscriptionsSymbol].unsubscribe(); } onDestroy.call(ctx); };
Die Möglichkeiten dieses Dekorateurs können nicht als umfassend bezeichnet werden. Sie decken nicht alle möglichen Verwendungen ab, die in einer großen Anwendung auftreten können. Dies sind beispielsweise Aufrufe von Vorlagenfunktionen, die Observable-Objekte zurückgeben. Aber ich arbeite daran.
Trotzdem reicht der obige Dekorateur für mein kleines Projekt. Den vollständigen Code finden Sie am Ende des Materials.
Analyse der Ergebnisse der Anwendungsbeschleunigung
Nachdem wir uns ein wenig mit den internen Mechanismen von Ivy befasst haben und erklärt haben, wie man mit diesen Mechanismen einen Dekorateur erstellt, ist es an der Zeit zu testen, was wir in einer echten Anwendung haben.
Um herauszufinden, wie sich zone.js auf die Leistung von Angular-Anwendungen
auswirkt , habe ich mein
Cryptofolio- Hobbyprojekt verwendet.
Ich habe den Dekorator auf alle erforderlichen Links angewendet, die in den Vorlagen und in disabled zone.js verwendet werden. Betrachten Sie beispielsweise die folgende Komponente:
@Component({...}) export class AssetPricerComponent { @observed() price$: Observable<string>; @observed() trend$: Observable<Trend>;
In der Vorlage werden zwei Variablen verwendet:
price
(der Asset-Preis wird hier angezeigt) und
trend
(diese Variable kann Werte annehmen, die
stale
oder
down
und die Richtung der Preisänderung angeben). Ich habe sie mit
@observed
dekoriert.
▍ Projektpaketgröße
Schauen wir uns zunächst an, wie stark sich die Größe des Projektbündels verringert hat, während zone.js entfernt wurde. Unten sehen Sie das Ergebnis der Erstellung des Projekts mit zone.js.
Ergebnis der Erstellung eines Projekts mit zone.jsUnd hier ist die Versammlung ohne zone.js.
Das Ergebnis der Erstellung eines Projekts ohne zone.jspolyfills-es2015.xxx.js
. Wenn das Projekt zone.js verwendet, beträgt seine Größe ungefähr 35 KB. Aber ohne zone.js - nur 130 Bytes.
▍Booten
Ich habe zwei Anwendungsmöglichkeiten mit Lighthouse untersucht. Die Ergebnisse dieser Studie sind unten angegeben. Es sollte beachtet werden, dass ich sie nicht zu ernst nehmen würde. Tatsache ist, dass ich beim Versuch, die Durchschnittswerte zu ermitteln, signifikant unterschiedliche Ergebnisse erzielt habe, indem ich mehrere Messungen für dieselbe Anwendungsversion durchgeführt habe.
Möglicherweise hängt der Unterschied bei der Bewertung der beiden Anwendungsoptionen nur von der Größe der Bundles ab.
Hier ist das Ergebnis für eine Anwendung, die zone.js verwendet.
Analyseergebnisse für eine Anwendung, die zone.js verwendetUnd hier ist, was nach der Analyse der Anwendung passiert ist, in der zone.js nicht verwendet wird.
Analyseergebnisse für eine Anwendung, die zone.js nicht verwendet▍ Leistung
Und jetzt kamen wir zu den interessantesten. Dies ist die Leistung einer Anwendung, die unter Last ausgeführt wird. Wir möchten herausfinden, wie sich der Prozessor fühlt, wenn die Anwendung mehrmals pro Sekunde Preisaktualisierungen für Hunderte von Assets anzeigt.
Um die Anwendung zu laden, habe ich 100 Entitäten erstellt, die bedingte Daten zu Preisen bereitstellen, die sich alle 250 ms ändern. Wenn der Preis steigt, wird er grün angezeigt. Wenn reduziert - rot. All dies könnte mein MacBook Pro ernsthaft belasten.
Es ist anzumerken, dass ich bei der Arbeit im Finanzsektor an mehreren Anwendungen für die Hochfrequenzübertragung von Datenfragmenten oft auf eine ähnliche Situation gestoßen bin.
Um zu analysieren, wie verschiedene Versionen der Anwendung Prozessorressourcen verwenden, habe ich die Chrome-Entwicklertools verwendet.
So sieht die Anwendung aus, die zone.js. verwendet
Systemlast, die von einer Anwendung erstellt wurde, die zone.js verwendetUnd so funktioniert eine Anwendung, in der zone.js nicht verwendet wird.
Systemlast, die von einer Anwendung erstellt wurde, die zone.js nicht verwendetWir analysieren diese Ergebnisse unter Berücksichtigung des Prozessorlastdiagramms (gelb):
- Wie Sie sehen können, lädt eine Anwendung, die zone.js verwendet, den Prozessor ständig um 70-100%! Wenn Sie die Browser-Registerkarte für längere Zeit geöffnet halten und eine solche Last auf dem System erstellen, schlägt die darin ausgeführte Anwendung möglicherweise fehl.
- Und die Version der Anwendung, in der zone.js nicht verwendet wird, erzeugt eine stabile Auslastung des Prozessors im Bereich von 30 bis 40%. Großartig!
Beachten Sie, dass diese Ergebnisse bei geöffnetem Chrome Developer Tools-Fenster erzielt wurden, wodurch auch das System belastet und die Anwendung verlangsamt wird.
▍ Lasterhöhung
Ich habe versucht sicherzustellen, dass jedes Unternehmen, das für die Aktualisierung des Preises verantwortlich ist, zusätzlich zu dem, was es bereits produziert, jede Sekunde 4 weitere Updates herausgibt.
Folgendes haben wir über die Anwendung herausgefunden, in der zone.js nicht verwendet wird:
- Diese Anwendung ist normalerweise mit der Last fertig geworden und beansprucht nun etwa 50% der Prozessorressourcen.
- Er schaffte es, den Prozessor genauso wie die Anwendung mit zone.js zu laden, nur wenn die Preise alle 10 ms aktualisiert wurden (neue Daten stammten wie zuvor von 100 Entitäten).
▍ Leistungsanalyse mit Angular Benchpress
Die Performance-Analyse, die ich oben durchgeführt habe, kann nicht als besonders wissenschaftlich bezeichnet werden. Für eine eingehendere Untersuchung der Leistung verschiedener Frameworks würde ich die Verwendung
dieses Benchmarks empfehlen. Für Recherchen sollte Angular die übliche Version dieses Frameworks und seine Version ohne zone.js auswählen.
Ich habe, inspiriert von einigen Ideen dieses Benchmarks, ein
Projekt erstellt , das umfangreiche Berechnungen durchführt. Ich habe seine Leistung mit
Angular Benchpress getestet .
Hier ist der Code der getesteten Komponente:
@Component({...}) export class AppComponent { public data = []; @observed() run(length: number) { this.clear(); this.buildData(length); } @observed() append(length: number) { this.buildData(length); } @observed() removeAll() { this.clear(); } @observed() remove(item) { for (let i = 0, l = this.data.length; i < l; i++) { if (this.data[i].id === item.id) { this.data.splice(i, 1); break; } } } trackById(item) { return item.id; } private clear() { this.data = []; } private buildData(length: number) { const start = this.data.length; const end = start + length; for (let n = start; n <= end; n++) { this.data.push({ id: n, label: Math.random() }); } } }
Ich habe eine kleine Reihe von Benchmarks mit Protractor und Benchpress gestartet. Die Operationen wurden eine festgelegte Anzahl von Malen durchgeführt.
Bankdrücken in AktionErgebnisse
Hier finden Sie eine Auswahl der mit Benchpress erzielten Ergebnisse.
Benchpress-ErgebnisseHier ist eine Erläuterung der in dieser Tabelle dargestellten Indikatoren:
gcAmount
: gc-Betriebsvolumen (Speicherbereinigung), Kb.gcTime
: gc Betriebszeit, ms.majorGcTime
: Zeit der Hauptoperationen gc, ms.pureScriptTime
: Skriptausführungszeit in ms ohne gc-Operationen und Rendering.renderTime
: Renderzeit, ms.scriptTime
: Skriptausführungszeit unter Berücksichtigung von gc-Operationen und Rendering.
Nun betrachten wir die Analyse der Leistung einiger Operationen in verschiedenen Anwendungsvarianten. Grün zeigt die Ergebnisse einer Anwendung, die zone.js verwendet. Orange zeigt die Ergebnisse einer Anwendung ohne zone.js. Bitte beachten Sie, dass hier nur die Renderzeit ausgewertet wird. Wenn Sie an allen Testergebnissen interessiert sind, klicken Sie
hier .
Test: 1000 Zeilen erstellen
Im ersten Test werden 1000 Zeilen erstellt.
TestergebnisseTest: Erstellen von 10.000 Zeilen
Je höher die Auslastung der Anwendungen ist, desto größer ist auch der Unterschied in der Leistung.
TestergebnisseTest: 1000 Zeilen verbinden
In diesem Test werden 1000 Zeilen an 10.000 Zeilen angehängt.
TestergebnisseTest: Entfernen von 10.000 Zeilen
Hier werden 10.000 Zeilen angelegt, die dann gelöscht werden.
TestergebnisseTypeScript Decorator-Quellcode
Unten finden Sie den Quellcode des hier beschriebenen TypeScript-Dekorators. Diesen Code finden Sie auch
hier .
Zusammenfassung
Obwohl ich hoffe, dass Ihnen meine Geschichte über die Optimierung der Leistung von Angular-Projekten gefallen hat, hoffe ich, dass Sie nicht dazu veranlasst werden, zone.js schnell aus Ihrem Projekt zu entfernen. Die hier beschriebene Strategie sollte der allerletzte Ausweg sein, auf den Sie zurückgreifen können, um die Leistung Ihrer Angular-Anwendung zu steigern.
Zunächst müssen Sie Ansätze wie die OnPush-Änderungserkennungsstrategie
trackBy
,
trackBy
anwenden, Komponenten deaktivieren, Code außerhalb von zone.js ausführen und zone.js-Ereignisse auf die schwarze Liste setzen (diese Liste von Optimierungsmethoden kann fortgesetzt werden). Der hier gezeigte Ansatz ist ziemlich teuer, und ich bin mir nicht sicher, ob jeder bereit ist, einen so hohen Preis für die Leistung zu zahlen.
Tatsächlich ist die Entwicklung ohne zone.js möglicherweise nicht die attraktivste Sache. Vielleicht ist dies nicht nur für die Person, die an dem Projekt beteiligt ist, das unter seiner vollen Kontrolle steht. Das heißt - es ist der Eigentümer von Abhängigkeiten und hat die Fähigkeit und Zeit, alles in die richtige Form zu bringen.
Wenn sich herausstellt, dass Sie alles ausprobiert haben und glauben, dass der Engpass Ihres Projekts genau zone.js ist, sollten Sie möglicherweise versuchen, Angular zu beschleunigen, indem Sie die Änderungen unabhängig voneinander erkennen.
Ich hoffe, dass Sie in diesem Artikel sehen konnten, was Angular in Zukunft erwartet, wozu Ivy in der Lage ist und was mit zone.js getan werden kann, um die Anwendungsgeschwindigkeit zu maximieren.
Sehr geehrte Leser! Wie optimieren Sie Ihre Angular-Projekte, die maximale Leistung benötigen?
