Angular erlaubt es, in Templates direkt Methoden aufzurufen, z. B. innerhalb von Strukturdirektiven wie *ngIf
oder *ngFor
sowie in Property-Bindings ([value]
, {{ }}
Interpolation usw.). Obwohl dies bequem und technisch möglich ist, wird es allgemein als schlechte Praxis angesehen. Im Folgenden wird erläutert, warum Funktionsaufrufe in Angular-Templates problematisch sein können. Dabei betrachten wir die Performance-Auswirkungen, das Zusammenspiel mit der Change Detection, die Lesbarkeit und Wartbarkeit des Codes sowie Best Practices und empfohlene Alternativen. Abschließend wird beschrieben, in welchen Fällen es ausnahmsweise akzeptabel sein könnte und wann man es unbedingt vermeiden sollte.
Performance und Change Detection: Warum Funktionsaufrufe teuer sind
Hauptproblem: Jedes Mal, wenn Angular eine Change-Detection-Runde durchführt, werden alle Template-Ausdrücke ausgewertet. Das bedeutet, ein Funktionsaufruf im Template wird bei jeder Change Detection erneut ausgeführt – selbst wenn dessen Rückgabewert unverändert bleibt. Angulars Change Detection überprüft standardmäßig ständig, ob sich Ausdruckswerte geändert haben, kann aber bei einer Funktion nicht wissen, ob das Ergebnis dasselbe geblieben ist, ohne die Funktion erneut aufzurufen.
Beispiel: Angenommen, wir haben in einem Template
*ngIf="istBerechtigt()"
. Wenn diese Methode intern z. B. Benutzerrechte prüft, wirdistBerechtigt()
bei jeder Änderung (Events, Timer, HTTP-Antworten etc.) zig Mal aufgerufen. Selbst ein einfachesconsole.log
darin würde zeigen, dass die Funktion potenziell Hunderte Male pro Sekunde ausgeführt wird. Warum? Weil Angular bei jeder Veränderung im Angular-App-Kontext (auch außerhalb der Komponente) eine Change Detection anstößt, in der die Template-Funktion erneut ausgewertet wirdSkalierungsproblem: Die Aufrufe multiplizieren sich, wenn sie innerhalb von Strukturdirektiven verwendet werden. Ein Funktionsaufruf in einer
*ngFor
-Schleife wird für jedes Element und bei jeder Change Detection erneut ausgeführt. Hat die Liste 100 Einträge und Change Detection läuft 10 Mal, ergibt das 1000 Funktionsausführungen. Angular ruft eine Template-Funktion also im schlimmsten Fall N × M Mal auf (N = Change-Detection-Durchläufe, M = Anzahl betroffener Template-Bindungen), egal ob das Resultat sich geändert hat oder nicht.
Kurz gesagt, eine Template-Funktion wird unter Angulars Standard-Change-Detection immer wiederholt ausgewertet – was zu unnötiger CPU-Belastung führen kann, wenn man sie nicht vermeidet.
Change Detection und Seiteneffekte
Angulars Change Detection läuft sehr häufig – standardmäßig bei jedem Benutzerevent, setTimeout
, HTTP-Response und vielen anderen Asynchronitäten (Zone.js fängt diese auf). Somit kann auch eine scheinbar harmlose Template-Funktion plötzlich tausendfach aufgerufen werden, wenn z. B. Mausbewegungen oder wiederkehrende Timer im Spiel sind. Ein Beispiel aus der Praxis: Eine Komponente zeigt einen Namen mit {{ fullName() }}
an. Solange kaum Events stattfinden, mag das unkritisch sein. Fügt man später jedoch z. B. ein Mausbewegungs-Event im Template hinzu, wird fullName()
jedes Mal bei Mousemove aufgerufen – also unter Umständen Hunderte Male pro Sekunde
Template-Funktionen sollten rein (ohne Nebeneffekte) sein. Sollte eine Funktion im Template doch Seiteneffekte haben (z. B. interne Änderungen am Component State vornehmen), könnte das einen Loop auslösen. Denn eine State-Änderung triggert wieder die Change Detection, die wiederum die Funktion erneut aufruft. Dies führt zu unerwarteten Change-Detection-Schleifen und instabilem Verhalten. Daher sollte man Logik mit Zustandsänderungen nie in Template-Aufrufe packen.
OnPush und Sonderfälle
Angular bietet mit ChangeDetectionStrategy.OnPush
eine Möglichkeit, Change Detection nur bei bestimmten Bedingungen (z. B. geänderte Input-Referenzen) auszuführen. OnPush kann das Problem abmildern, da die betreffende Komponente dann nicht auf jede globale Änderung reagiert. Eine Template-Funktion wird bei OnPush-Komponenten also seltener aufgerufen – nämlich nur, wenn Input-Daten sich ändern oder ein manuelles markForCheck
erfolgt. Aber Achtung: Selbst mit OnPush werden Funktionsaufrufe nicht gecacht – ändern sich die Inputs häufig oder ist die Funktion komplex, kann es weiterhin zu Performanceproblemen kommen. OnPush reduziert die Frequenz, ändert aber nichts daran, dass Angular die Funktion bei jeder eigenen Change-Detection-Runde der Komponente neu auswertet, weil es nicht weiß, ob das Ergebnis gleich geblieben ist.
Ein legitimer Sonderfall, wo eine Funktion im Template vorgesehen ist, sind trackBy
-Funktionen in *ngFor
. Diese werden bewusst eingesetzt, um Angular bei der Effizienz von Listen-Renderings zu helfen. Die trackBy
-Funktion wird von Angular jedoch nur bei Änderungen in der Liste aufgerufen und dient einem speziellen Zweck (Identifizieren von Listenelementen). Solche Funktionen sind in der Regel sehr einfach (z. B. Rückgabe einer ID) und von Angular optimiert gehandhabt. Außer solchen speziellen Fällen gibt es kaum Gründe, eigene Funktionen direkt im Template zu benutzen.
Lesbarkeit und Wartbarkeit des Codes
Neben Performance-Aspekten spielt auch die Code-Qualität eine Rolle. Template-Funktionsaufrufe verstecken Logik innerhalb der View-Schicht, was den Code schwerer lesbar und für andere Entwickler weniger offensichtlich macht. Man stelle sich ein Template voller Funktionsaufrufe vor – um zu verstehen, was dort passiert, muss man ständig in die Component-Klasse springen und die Funktionen nachschlagen. Wie ein Blogbeitrag es formuliert: Funktionen im Template können den Code „schwerer zu lesen und zu verstehen“ machen, da die Logik im Template versteckt ist. Eine komplexe Ausdruckslogik gehört in der Regel in die Komponente oder in eigene Services/Pipes, damit die HTML-Template sauber und deklarativ bleibt.
Wartbarkeit: Wenn Logik verstreut in Templates steckt, wird es schwieriger, diese Logik wiederzuverwenden oder zu testen. Eine Berechnung in einer Component-Methode kann man relativ leicht in Unit-Tests prüfen. Dieselbe Berechnung, die nur im Template als Funktionsaufruf existiert, ist zwar technisch testbar (über den Methodenaufruf), aber ihr häufiger Aufruf und Einfluss auf das Rendering erschweren das isolierte Testen der Komponente. Außerdem erhöht es die Gefahr, dass Änderungen an der Funktion unbemerkt Performance oder Verhalten beeinflussen. Im Worst Case ändert ein Entwickler eine solche Funktion, ohne zu realisieren, dass sie hunderte Male pro Sekunde ausgeführt wird – ein Performance-Bug, der sich schwer debuggen lässt.
Best Practices und empfohlene Alternativen
Wie kann man also die oben genannten Probleme vermeiden? Es gibt mehrere bewährte Alternativen zu Funktionsaufrufen direkt im Template:
1. Vorausberechnen und Zwischenspeichern (Komponenten-Property nutzen):
Statt im Template {{ computeValue() }}
aufzurufen, berechnet man den Wert einmalig oder bei Bedarf in der Component-Klasse und speichert ihn in einem Feld. Dieses Feld kann im Template via Interpolation oder Binding direkt genutzt werden.
2. Verwendung von Pipes (Transformationslogik auslagern):
Für formatierende oder berechnende Logik bieten sich Angular Pipes an. Ein pure Pipe (Standard in Angular, sofern pure: false
nicht gesetzt wird) wird nur neu berechnet, wenn sich seine Input-Werte änderndzone.com. Angular cached also das Ergebnis für gegebene Inputs. Das ist ideal, um teure Berechnungen zu vermeiden, wenn sich nichts geändert hat. Man kann eigene Pipes schreiben, um Logik aus dem Template auszulagern.
Beispiel: Anstatt {{ getMemberShipLevel(member.point) }}
im Template aufzurufen (siehe vorheriges Beispiel), definiert man eine Pipe:
@Pipe({ name: 'membershipLevel', pure: true })
export class MembershipLevelPipe implements PipeTransform {
transform(points: number): string {
if (points > 900) return 'Platinum';
else if (points > 700) return 'Gold';
else if (points > 500) return 'Silver';
return 'Basic';
}
}
Verwendung im Template:
{{ member.point | membershipLevel }}
3. Einsatz von Observables und AsyncPipe (für dynamische Daten):
Wenn der Wert, den man ursprünglich per Template-Funktion ermitteln wollte, aus asynchronen Daten stammt (z. B. einem Service, einer HTTP-Anfrage, Router-Parameter, WebSocket etc.), ist es empfehlenswert, mit Observables und dem async
Pipe zu arbeiten, statt eine getXyz()-Methode ständig pollen zu lassen.
Beispiel: Statt *ngIf="istRouteAktiv('/dashboard')"
eine Observable für die aktuelle Route verwenden:
// Component.ts
aktuelleRoute$: Observable<string> = this.router.events.pipe(
// Observable, das auf Routenwechsel reagiert und die URL liefert
map(() => this.router.url)
);
Im Template:
<!-- Template: async pipe gibt den aktuellen Wert aus der Observable -->
<app-profile-pill *ngIf="(aktuelleRoute$ | async) === '/dashboard'"> ... </app-profile-pill>