Schnellerer Angular durch weniger Arbeit: Zehn Techniken jenseits von OnPush und trackBy
Vermeiden Sie überflüssige Netzwerkaktivitäten, reaktive Abläufe sowie DOM- und CPU-Auslastung in modernem Angular mithilfe von @defer, inkrementeller Hydratierung, resource(), berechneten Signalen, Render-Phasen und Web Workers.
Quellen: How to Make Angular Extremely Fast: 10 Performance Tricks Most Developers Miss
Der Laufzeitumgebung von Angular ist selten der Grund dafür, dass eine Angular-Anwendung langsam wirkt. Viel häufiger lädt die Anwendung Dinge herunter, initialisiert sie, berechnet sie erneut und rendernt sie – obwohl niemand danach gefragt hat. Die bekannten Lösungen wie OnPush, lazy Routes, trackBy, AOT-Kompilierung, Produktionsbauten sowie weniger HTTP-Aufrufe helfen zwar weiter, doch moderne Angular-Versionen bieten viel feinere Kontrolle: Wann Code heruntergeladen wird, wann Komponenten interaktiv werden, wann Anfragen storniert werden, welcher Zustand ungültig wird, wann DOM-Arbeiten ausgeführt werden und auf welchem Thread schwere Berechnungen stattfinden. Dieser Leitfaden zeigt zehn Techniken, die auf dieser Kontrolle basieren, wie man sie kombinieren kann und in welcher Reihenfolge man sie anwenden sollte, nachdem man ermittelt hat, wohin die Zeit tatsächlich geht.
Die Leitidee ist einfach: Anstatt Angular bei unnötiger Arbeit schneller zu machen, sollte man die Dinge so gestalten, dass diese Arbeit überhaupt nicht stattfindet.
Wo eine Angular-Anwendung ihre Zeit verbringt
Bevor man sich mit den einzelnen APIs beschäftigt, ist es hilfreich, sich die drei Ressourcen vorzustellen, die jede Angular-Anwendung verbraucht, und herauszufinden, was typischerweise jeweils davon in Anspruch genommen wird.
Angular Application
│
┌────────────────┼────────────────┐
│ │ │
Network CPU DOM
│ │ │
JS downloads Change detection Layout
API requests Computation Paint
Dependencies Parsing Reflow
│ │ │
└────────────────┼────────────────┘
│
User Experience
Diese Kosten wirken miteinander zusammen. Das Anpassen der Änderungserkennung bringt wenig, wenn beim ersten Seitenladen 5 MB an JavaScript heruntergeladen werden und ein kleiner Bundle keinen Vorteil bietet, wenn jede Interaktion eine aufwändige Berechnung auslöst. Das Ziel sollte sein: weniger herunterladen, weniger initialisieren, weniger berechnen, weniger rendern und den Hauptthread seltener blockieren. Die zehn unten aufgeführten Techniken zielen jeweils auf einen oder mehrere dieser Schritte ab.
1. Stellen Sie sicher, dass @defer tatsächlich getrennte Blöcke erzeugt
Der @defer-Block ist eine der einfachsten Methoden, um die beim Start benötigte JavaScript-Menge zu verringern. Stellen Sie sich ein Dashboard mit Diagrammen, Analysen, Berichten, Karten, Video-Playern und Admin-Widgets vor. Nur wenige davon müssen bereits beim Öffnen der Seite vorhanden sein, doch ein einfaches Template lädt sie alle sofort herunter:
<app-heavy-chart />
<app-map />
<app-report />
Wenn ein ressourcenintensiver Komponent in einen deferred-Block eingebettet wird, weist Angular an, seinen Code erst dann abzurufen, wenn ein Auslöser aktiviert wird – in diesem Fall, wenn der Platzhalter in den Sichtbereich scrollt:
@defer (on viewport) {
<app-heavy-chart />
}
Halten Sie deferred-Komponenten außerhalb von Barrel-Imports
Es gibt eine feine Einschränkung: @defer kann nur Code trennen, der nicht bereits über einen sofortigen Import erreichbar ist. Wenn die Host-Komponente die deferred-Komponente über eine Barrel-Datei importiert, zieht diese alle anderen Exporte sowie deren Abhängigkeiten in denselben Importgraphen hinein. Anstatt also von einer index-Datei zu importieren:
import { HeavyComponent } from './index';
Importieren Sie das Komponente aus seiner eigenen Datei:
import { HeavyComponent } from './heavy.component';
Ziel ist es, den Abhängigkeitsgraphen hinter jedem verzögerten Block so isoliert wie möglich zu halten. Die erste Form, der man ausweichen sollte, ist wie folgt gekennzeichnet:
BAD
Und es sieht so aus, mit einem einzigen „Barrel“, der jedes Widget mit dem Dashboard verbindet:
Dashboard
│
└── index.ts
├── Chart
├── Map
├── Reports
├── Video
└── Analytics
Die bevorzugte Form ist wie folgt gekennzeichnet:
GOOD
Und sie ermöglicht eine direkte Importierung des aufwändigen Diagramms sowie somit einen eigenen Datenblock dafür:
Dashboard
│
└── HeavyChart
│
└── heavy-chart.chunk.js
„Barrel“-Importe sind einer der häufigsten versteckten Gründe dafür, dass ein @defer-Block die anfängliche Dateigröße nicht verringert. Die einfachste Möglichkeit, Erfolg zu überprüfen, besteht darin, nach Hinzufügen des Blocks in der Build-Ausgabe nach einem neuen, verzögerten Datenblock zu suchen.
Der Nutzen verstärkt sich: Ein kleinererer Anfangspaket bedeutet weniger zu laden, zu analysieren, zu kompilieren und auszuführen – und all diese Einsparungen erfolgen bereits bevor Angular etwas darstellt.
2. Hydratisieren nur das, was die Benutzer zuerst benötigen
Beim Server-side Rendering tritt nach Erhalt des HTML ein weiteres Engpass auf. Der Server kann die Markup-Strukturen schnell erstellen, doch der Browser muss anschließend die gesamte Seite interaktiv machen, was das Herunterladen und Ausführen des Codes für jede Komponente erfordert. Betrachten wir eine typische Dashboard-Anordnung:
Dashboard
│
├── Header
├── Navigation
├── KPI Cards
├── Huge Chart
├── Map
├── Analytics Table
├── Video Player
└── Recommendations
Nicht alle dieser Abschnitte müssen sofort auf Eingaben reagieren. Die klassische vollständige Hydratisierung behandelt die Seite als eine Einheit:
SSR HTML
↓
Hydrate EVERYTHING
↓
Interactive
Die schrittweise Hydratisierung ermöglicht es, dies in Phasen aufzuteilen, sodass die Seite sofort sichtbar ist und die Abschnitte schrittweise interaktiv werden:
SSR HTML
↓
Show page immediately
↓
Hydrate only what is needed
↓
Hydrate other sections later
Dies wird mit denselben @defer-Blöcken dargestellt, die durch Hydrationsauslöser erweitert werden. Im folgenden Beispiel rendernt der Server weiterhin die HTML-Struktur des Charts, der Client hydratiert sie, wenn der Browser inaktiv ist, und bei einer rein clientseitigen Darstellung wird der Block beim Interagieren geladen:
@defer (on interaction; hydrate on idle) {
<app-heavy-chart />
}
Der architektonische Vorteil besteht darin, dass das Rendern von HTML und die Erstellung interaktiver Funktionen nicht mehr als ein einziger, alles-oder-nichts-Schritt erfolgen. Eine schrittweise Hydratierung muss in der Hydrationskonfiguration aktiviert werden; prüfen Sie die SSR-Anleitung für Ihre Angular-Version nach der genauen Provider-Option.
3. Überflüssige Anfragen mit resource() abbrechen
Suchen während des Eintippens ist ein klassisches Beispiel für verschwendete Netzwerkressourcen. Während der Benutzer eintippt, durchläuft die Eingabe alle Zwischenwerte:
a
an
ang
angu
angul
angula
angular
Eine naive Implementierung kann für jeden dieser Zwischenwerte eine Anfrage senden:
Request 1 → "a"
Request 2 → "an"
Request 3 → "ang"
Request 4 → "angu"
Request 5 → "angul"
Request 6 → "angular"
Nur die letzte Antwort ist relevant, daher sind die anderen fünf überflüssig. Moderne Angular-Versionen bieten resource() und httpResource() für reaktives, asynchrones Laden an. Man beginnt mit einem Signal, das die Abfrage enthält:
const query = signal('');
Dann deklariert man eine Ressource, deren params-Funktion dieses Signal liest und deren loader die Anfrage ausführt. In der folgenden Darstellung wurde loader auf derselben Zeile wie der schließende Klammernzeichen von params platziert; beides sind getrennte Eigenschaften des Options-Objekts:
const results = resource({
params: () => ({
q: query()
}), loader: ({ params, abortSignal }) =>
fetch(
`/api/search?q=${params.q}`,
{ signal: abortSignal }
).then(res => res.json())
});
Sobald sich die Parameter während eines noch laufenden Ladevorgangs ändern, beendet Angular den vorherigen Vorgang. Das funktioniert nur, weil der Loader das übergebene Signal an fetch weiterleitet – darauf muss man achten:
{ signal: abortSignal }
Wenn man das weglässt, läuft die alte Anfrage bis zum Ende weiter. In echtem Code lohnen sich zwei Verbesserungen: Man sollte die Abfrage vor der Einbettung in die URL mit encodeURIComponent umschließen und bedenken, dass die Abbruchfunktion keine Verzögerungstaktik darstellt. Jeder Tastendruck startet weiterhin eine Anfrage, die anschließend abgebrochen wird; daher sollte man Ressourcen mit einer verzögerten Abfrageanweisung kombinieren, wenn man auch deren Versand vermeiden möchte. Die schnellste Anfrage ist diejenige, die man sofort stoppt, sobald sie irrelevant wird – dieses Vorgehen ist besonders vorteilhaft bei Suchfunktionen, Autocomplete-Funktionen, Filtern, Echtzeit-Dashboards sowie bei schnell wechselnden IDs und Ketten abhängiger Aufrufe.
4. Halten Sie Signale klein und fokussiert
Signale sind effizient, können aber dennoch so verwendet werden, dass diese Effizienz verloren geht. Ein häufiges Muster ist es, ein einziges Signal mit dem gesamten Zustand einer Seite zu füllen:
const pageState = signal({
user: {},
filters: {},
notifications: [],
products: [],
statistics: {},
settings: {}
});
Falls nur die Filter sich ändern, wird jeder Verbraucher von pageState benachrichtigt, da der gesamte Wert des Signals ersetzt wurde. Das Aufteilen unabhängiger Zustandskomponenten in separate Signale verringert den Einflussbereich. Das erste Signal wird wie folgt:
const user = signal(...);
und die übrigen folgen dem gleichen Muster (hier zusammen auf einer Zeile dargestellt; im echten Code steht jedes auf eigener Zeile):
const filters = signal(...);const notifications = signal(...);const products = signal(...);const statistics = signal(...);const settings = signal(...);
Nun hängt jeder Verbraucher nur vom Zustand ab, den er tatsächlich liest. Eine Tabellenkalkulation ist ein gutes mentales Modell dafür. Wenn sich eine einzelne Zelle ändert:
A1 changes
möchte man nicht, dass die gesamte Tabelle neu berechnet wird, sondern nur der Teil, der davon abhängt:
A1
↓
Dependent cells
↓
Only affected UI
Ein großes Objektsignal macht alle Verbraucher ungültig, die sich nur für einen Teil davon interessieren; kleine Signale bieten enge Gültigkeitsbereiche. Statt zu prüfen, ob der gesamte Zustand in ein einziges Signal passt, sollten Sie fragen, was eigentlich reagieren soll, wenn sich ein bestimmter Wert ändert. Falls Sie tatsächlich ein komplexes Objekt benötigen, leiten Sie es mit computed() aus den kleinen Signalen ab, anstatt es als Quelle der Wahrheit zu speichern.
5. Teure Berechnungen aus Templates entfernen
Die Aufrufung einer Funktion aus einem Template ist eine der einfachsten Möglichkeiten, wiederholte Arbeit zu erzeugen:
<div>
{{ calculateTotal(items()) }}
</div>
Es scheint harmlos zu sein, aber Angular bewertet diesen Ausdruck jedes Mal neu, wenn die Ansicht überprüft wird, sodass eine aufwändige calculateTotal()-Funktion immer wieder ausgeführt wird, selbst wenn sich items nicht geändert hat. Die Lösung ist ein computed()-Signal. Deklarieren Sie den Ausgangszustand:
const items = signal<Item[]>([]);
Erhalten Sie den Gesamtwert daraus:
const total = computed(() =>
items().reduce(
(sum, item) => sum + item.price,
0
)
);
Und lesen Sie das abgeleitete Signal im Template:
<div>
{{ total() }}
</div>
computed() speichert sein Ergebnis und berechnet es erneut nur, wenn sich ein von ihm gelesenes Signal geändert hat – anstatt bei jeder Template-Auswertung. Dadurch eignet es sich hervorragend für Filtern, Sortieren, Aggregationen, Statistiken, Dashboard-Daten und jeden anderen abgeleiteten UI-Zustand. Eine gefilterte Produktliste folgt derselben Struktur und beginnt mit einem Suchbegriffs-Signal:
const searchTerm = signal('');
Und eine abgeleitete Liste, die sowohl von den Produkten als auch vom Suchbegriff abhängt:
const filteredProducts = computed(() =>
products().filter(product =>
product.name.includes(searchTerm())
)
);
Das ersetzt Template-Aufrufe wie den folgenden und verlagert die Verarbeitung in das reaktive Graphen-System:
{{ filterProducts(products(), searchTerm()) }}
Ein Detail: includes ist casesensitiv, daher würde eine echte Suche in der Regel zunächst beide Zeichenketten normalisieren. Betrachten Sie computed() als Cache für abgeleiteten Zustand, der sich selbst korrekt ungültig macht.
6. Laden Sie schwere Dienste verspätet, nicht nur Komponenten
Das Verspätete Laden wird in der Regel auf zwei Ebenen diskutiert:
Lazy route
Lazy component
Dienste können jedoch ihre eigenen großen Abhängigkeitsbäume mitbringen. Ein gutes Beispiel ist ein Markdown-Voransichtsdienst:
MarkdownService
│
├── marked
├── highlight.js
├── syntax definitions
└── other dependencies
Falls der Dienst nur dann verwendet wird, wenn jemand auf die Schaltfläche „Markdown voranschauen“ klickt, gibt es keinen Grund, den Parser, den Syntax-Highlighter sowie deren Grammatiken im initialen Bundle mitzuliefern. Die Technik besteht darin, den Dienst durch eine dynamische Importierung genau dann zu laden, wenn er benötigt wird. Konzeptionell sieht das so aus, unter Verwendung eines injectAsync()-Hilfsfunktionen:
const markdownService = injectAsync(
() =>
import('../markdown.service')
.then(m => m.MarkdownService)
);
Dann wartet der Komponente auf den Service innerhalb des Handlers, der ihn benötigt:
async preview(content: string) {
const service = await this.markdownService();
und verwendet ihn zur Darstellung des Inhalts:
this.html = service.render(content);
}
Betrachten Sie injectAsync() eher als Muster denn als garantierte Kern-API: Überprüfen Sie, ob Ihre Angular-Version oder eine von Ihnen verwendete Bibliothek diese bereitstellt. Derselbe Effekt lässt sich auch mit einem einfachen await import('../markdown.service') innerhalb des Handlers erzielen, gefolgt von der Auflösung der Klasse über den Injector. In jedem Fall landen der Service sowie seine aufwändigen Abhängigkeiten in ihrem eigenen Chunk. Die allgemeine Regel lautet, aufwändige Abhängigkeiten dort zu laden, wo sie nützlich sind – und nicht nur an den Routengrenzen.
7. Betrachten Sie DOM-Lesungen und -Schreibvorgänge als kostspielige API
Mannchmal liegt das Engpassproblem überhaupt nicht im JavaScript, sondern im Layout-Engine des Browsers. Das tritt auf, wenn Code Messungen und Änderungen miteinander vermischt:
read DOM
write DOM
read DOM
write DOM
read DOM
write DOM
Jeder Lesvorgang nach einem Schreibvorgang kann den Browser zwingen, das Layout synchron neu zu berechnen – ein Phänomen, das als Layout-Thrashing bezeichnet wird. Angular bietet dafür render-bewusste Hooks an. Der erste davon ist:
afterNextRender()
und der zweite, der explizite Phasen unterstützt, ist:
afterRenderEffect()
Mit afterRenderEffect() platzieren Sie Messungen in der earlyRead-Phase:
afterRenderEffect({
earlyRead: () => {
return element.getBoundingClientRect();
},
und Mutationen in der write-Phase, die den von der Lesung zurückgegebenen Wert erhält (dieser Abschnitt ist der Rest desselben TypeScript-Aufrufs, nicht CSS):
write: height => {
element.style.height = `${height}px`;
}
});
Angular führt die Lesungsphasen aller Komponenten vor ihren Schreibphasen aus, sodass der Browser die Aufgaben so gruppiert sieht:
READ
↓
READ
↓
READ
↓
WRITE
↓
WRITE
↓
WRITE
anstatt ständig abwechselnd zu arbeiten:
READ → WRITE → READ → WRITE
Diese phasenbasierte Koordination ist besonders wichtig für Diagramme, Karten, Animationen, das Verarbeiten von Größenanpassungen und Scrollen, Drittanbieter-UI-Bibliotheken sowie komplexe Dashboards, bei denen viele Komponenten nach jeder Darstellung den DOM verändern.
8. Erkennung von Batch-Änderungen durch Kombination von Ereignissen und Ausführungen
In an Zonen basierenden Anwendungen können mehrere Auslöser nacheinander ausgelöst werden, was logisch einer einzigen Benutzeraktion entspricht:
click
↓
event bubbles
↓
parent handler
↓
ngZone.run()
↓
another update
Ohne Kombination kann jeder Schritt seine eigene Änderungserkennung durchführen. Angular kann sie zusammenfassen, wenn Sie beide Optionen in provideZoneChangeDetection aktivieren:
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({
eventCoalescing: true,
runCoalescing: true
})
]
});
Anstelle einer Durchführung pro Ereignis:
Event A → Change Detection
Event B → Change Detection
Event C → Change Detection
Überschneidende Aufgaben werden zu einer einzigen Durchführung zusammengefasst:
Event A ─┐
Event B ─┼──→ One Change Detection
Event C ─┘
Dies ist besonders nützlich in Anwendungen mit vielen Ereignishandlern, stark belasteten WebSocket-Streams, häufigen Aufrufen von ngZone.run() oder hochinteraktiven Dashboards. Bedenken Sie, dass diese Optionen auf die auf Zone.js basierende Änderungserkennung zutreffen; neuere Angular-Versionen bevorzugen eine änderungserkennungslose Architektur, bei der die Scheduling-Logik bereits durch Signale und explizite Benachrichtigungen gesteuert wird. Überprüfen Sie daher vor dem Einsatz, welchen Modus Ihre Anwendung verwendet.
9. Finden Sie die Abhängigkeiten, die Sie nicht mitliefern wollten
Einige der größten Leistungsprobleme sind keine Angular-Probleme, sondern Importprobleme. Möglicherweise benötigen Sie nur eine einzige Funktion aus einer Bibliothek, doch Ihre Importstruktur kann viel mehr mitladen:
Application
│
└── Library
│
├── Feature A
├── Feature B
├── Feature C
├── Feature D
└── Feature E
wenn Sie eigentlich nur Folgendes verwendet haben:
Feature A
Deshalb gehört die Bundle-Analyse zu jeder Leistungsuntersuchung. Erstellen Sie Statistiken zur Produktionsbuild-Struktur:
ng build --configuration=production --stats-json
Dann prüfen Sie die Ausgabe, um zu erkennen, welche Dateien und Bibliotheken den Payload dominieren. Tools wie source-map-explorer arbeiten mit Quellkarten, daher aktivieren Sie sie in der Build-Phase, die Sie analysieren; das Format der Statistikdatei hängt vom Compiler ab, den Ihr Projekt verwendet, sodass Sie prüfen müssen, welchen Analyzer dieser unterstützt. Entscheidend ist nicht, ob die Anwendung langsam wirkt, sondern genau das, was Sie an den Browser senden. Sobald dies sichtbar ist, werden die meisten Optimierungen offensichtlich. Um späterige Rückschritte zu vermeiden, sollten Sie in Betracht ziehen, Leistungsbudgets in CI durchzusetzen.
10. Schwere Berechnungen vom Hauptthread trennen
Mannchmal ist das Framework in Ordnung, aber das Problem liegt im Algorithmus. Angenommen, ein Dashboard erhält ein Datensatz dieser Größe:
100,000 records
und führt in dem Browser Statistiken, Bildverarbeitung, Geometrie, umfangreiche Transformationen sowie komplexe Parsing- oder Aggregationsaufgaben durch. Auf dem Hauptthread konkuriert diese Arbeit mit allem anderen:
Main Thread
│
├── Angular
├── Rendering
├── User input
├── Animations
└── 100K-record calculation
Das Ergebnis sind Ruckler: eingefrorene Eingaben, stockende Animationen und verzögerte Darstellung. Ein Web Worker verlagert die Berechnungen auf einen separaten Thread und sendet nur das Ergebnis zurück:
Main Thread Worker
│ │
│──── large dataset ─────>│
│ │
│ Calculate
│ Transform
│ Analyze
│ │
│<──── result ─────────────│
│
Render UI
Die Angular CLI kann einen Worker erstellen und seine Konfiguration einrichten:
ng generate web-worker app
und die Komponente erstellt ihn mit einer im Modul definierten relativen URL, damit der Bundler ihn finden und kompilieren kann:
const worker =
new Worker(
new URL('./app.worker', import.meta.url)
);
Eine Worker-Instanz macht Angulars eigene Arbeitsabläufe nicht günstiger. Stattdessen verhindert sie, dass teure Anwendungslogik die Darstellung sowie die Benutzerinteraktion blockiert. Verwenden Sie Worker nur dann, wenn die Berechnungen tatsächlich aufwendig sind – schließlich werden Daten zwischen Threads durch strukturiertes Klonen kopiert, und dieser Datentransfer hat selbst Kosten. Für eine framework-unabhängige Betrachtung dieser Entscheidung siehe wann CPU-Arbeit in einen Web Worker verlagert werden sollte.
Kombination der Techniken in einem Dashboard
Es handelt sich dabei nicht um zehn isolierte Tricks; sie verstärken sich gegenseitig. Ein unoptimiertes Analytics-Dashboard erledigt alles auf einmal:
Browser
│
├── Download huge JS bundle
├── Initialize everything
├── Hydrate everything
├── Start every API request
├── Recalculate large state
├── Execute expensive template functions
├── Perform DOM reads/writes
└── Calculate large datasets on main thread
Durch die Anwendung dieser Techniken erledigt jede Schicht nur das, was im jeweiligen Moment erforderlich ist:
Browser
│
├── Smaller initial bundle
│ └── @defer
│
├── Partial interactivity
│ └── Incremental hydration
│
├── Only latest API request
│ └── resource/httpResource
│
├── Narrow reactive updates
│ └── Fine-grained signals
│
├── Cached derived values
│ └── computed()
│
├── Heavy services loaded later
│ └── injectAsync()
│
├── Coordinated DOM work
│ └── afterRenderEffect()
│
├── Fewer redundant CD cycles
│ └── Event coalescing
│
├── Smaller dependencies
│ └── Bundle analysis
│
└── Heavy computation elsewhere
└── Web Worker
Die App läuft nicht unbedingt schneller, sondern erledigt einfach weniger Arbeit.
Eine kompakte Leistungsformel
Eine Möglichkeit, diesen Ansatz zusammenzufassen, ist als Summe der Reduzierungen:
Performance
=
Less JavaScript
+
Less Network Work
+
Less Reactive Work
+
Less DOM Work
+
Less Main-Thread Work
Noch kürzer ausgedrückt: Ein schnelles Angular bedeutet, weniger Arbeit zu erledigen, sie später zu erledigen und nur dann zu erledigen, wenn sie benötigt wird.
Eine praktische Reihenfolge der Optimierung
Wenden Sie nicht alles auf einmal an. Messen Sie zunächst und arbeiten Sie dann in etwa dieser Reihenfolge durch die Schritte, da frühere Schritte in der Regel den größten Einfluss auf das Starten haben.
Zuerst reduzieren Sie den anfänglichen JavaScript-Code:
@defer
injectAsync()
bundle analysis
Zweitens reduzieren Sie die anfängliche Hydratierungsarbeit:
Incremental Hydration
Drittens eliminieren Sie unnötige Netzwerkarbeiten:
resource()
httpResource()
Viertens beschränken Sie die reaktive Invalidation:
Fine-grained signals
computed()
Fünftens verringern Sie die Render-Overhead:
afterRenderEffect()
afterNextRender()
Siebtens: Entfernen Sie die überflüssige Änderungserkennung:
eventCoalescing
runCoalescing
Achtens: Beseitigen Sie CPU-Engpässe:
Web Workers
Messen, bevor Sie optimieren
Die wichtigste Regel ist es, blind zu optimieren, zu vermeiden. Dies überall anzuwenden, ist keine Strategie:
OnPush
ebenso wenig ist es sinnvoll, irgendetwas davon einzuführen, ohne zu wissen, wo der Engpass liegt:
memoization
workers
RxJS operators
defer blocks
Profilieren Sie mit Chrome DevTools, Lighthouse, Angular DevTools, der Analyse des Produktionsbundels sowie dem Leistungspanel des Browsers. Stellen Sie anschließend die grundlegende Frage:
What is actually slow?
und eingrenzen Sie die Antwort auf eine Kategorie:
Network?
JavaScript?
Change detection?
Rendering?
Layout?
CPU?
Nur wenn Sie wissen, welcher Faktor am stärksten dominiert, sollten Sie die entsprechende Technik wählen. Messen Sie nach jeder Änderung erneut, um zu bestätigen, dass die Verbesserung tatsächlich vorliegt.
Von einer schnelleren Änderungserkennung zu weniger Arbeit
Die älteren Ratschläge zur Leistungssteigerung in Angular drehten sich meist um eine Frage:
"How can I make change detection faster?"
Modernes Angular fördert eine nützlichere Frage:
"Why am I running this work at all?"
Diese zweite Frage gilt für alle Ebenen. Warum soll dieser Komponente jetzt heruntergeladen werden? Warum soll dieser Abschnitt jetzt initialisiert werden? Warum soll eine HTTP-Anfrage abgeschlossen werden, obwohl der Benutzer bereits die Abfrage geändert hat? Warum soll ein ganzes Zustandsobjekt wegen eines einzigen Feldes ungültig gemacht werden? Warum soll ein Wert neu berechnet werden, dessen Eingabedaten sich nicht geändert haben? Warum soll am DOM manipuliert werden, bevor die Darstellung abgeschlossen ist? Warum sollen aufwändige Berechnungen im UI-Thread ausgeführt werden? Jede unnötige Operation ist eine Chance.
Kernpunkte
Die zehn Techniken zielen auf verschiedene Ebenen ab und beseitigen jeweils eine bestimmte Art von Verschwendung:
@deferin Kombination mit direkten Importen verringert die anfängliche JavaScript-Menge.- Individuelle Initialisierung verringert die anfängliche Arbeitslast bei der Initialisierung.
resource() mit einem Abbruchsignal entfernt veraltete Netzwerkarbeiten.computed() beendet wiederholte Berechnungen.afterRenderEffect()-Phasen verhindern Layout-Probleme.Die schnellste Angular-Anwendung ist nicht die, die JavaScript am schnellsten ausführt; sie ist die, die am wenigsten unnötiges JavaScript ausführt. Die gesamte Strategie lässt sich in einem Bild zusammenfassen:
MAKE ANGULAR FAST
│
┌──────────┴──────────┐
│ │
DO LESS DO LATER
│ │
┌─────┼─────┐ ┌─────┼─────┐
│ │ │ │ │ │
Less Less Less Defer Lazy Hydrate
JS CPU DOM code data later
│ │ │ │ │ │
└─────┴─────┴─────────┴─────┴─────┘
│
▼
FASTER EXPERIENCE
Messen, die verschwendete Arbeit ermitteln, sie entfernen und erneut messen.