Teil 2: Maßnahmen zur Performanceverbesserung von AngularJS

So wird’s schneller: Performante Webanwendungen mit AngularJS

Dr. Christian Straube

© iStockphoto / RichVintage

Die Analyse von Scopes und Digest Cycles hilft dabei, Performancebremsen in AngularJS zu entdecken. Es gibt viele Möglichkeiten, diese zu beheben oder erst gar nicht entstehen zu lassen. Doch nicht jede Maßnahme lässt sich einfach umsetzen und ist effektiv. Dieser Überblick hilft, die richtige Lösung zu finden.

Dieser Artikel stellt Maßnahmen vor, mit denen die Performance von AngularJS-Webapplikationen verbessert und Probleme vermieden werden können. Die beschriebenen Maßnahmen verändern den AngularJS-Core nicht, um Vorwärtskompatibilität, insbesondere bei der Verwendung von Package Managern wie Bower, zu unterstützen. Auch wenn Anpassungen am AngularJS-Core die Performance erheblich steigern können, überwiegen in den allermeisten Fällen die dadurch induzierten Nachteile.

Artikelserie: Performanceprobleme in AngularJS meistern

Teil 1: Grundlagen und Messmethoden
Teil 2: Maßnahmen zur Performanceverbesserung

Abb. 1: Übersicht über die Grundkonzepte von AngularJS, deren Zusammenhänge und Aufgaben

Abb. 1: Übersicht über die Grundkonzepte von AngularJS, deren Zusammenhänge und Aufgaben

Maßnahmengruppe 1: Reduktion der Watcher-Funktionen

Die erste Maßnahmengruppe reduziert die Anzahl der Watcher-Funktionen, die mit jedem Digest Cycle aufgerufen werden (Abb. 1, Kreis 1). Dies ist aus zwei Gründen notwendig. Zum einen muss für jeden Watcher eine Kopie des überwachten Scope-Elements gespeichert werden, um eine Veränderung des Objekts feststellen zu können, den „Dirty State“. Zum anderen muss bei jedem Digest-Cycle-Durchlauf die Dirty Check Expression des Watchers interpretiert  und ausgeführt werden, was potenziell zu einem umfangreichen Call Tree führen kann.

Abb. 2: Beispielhaftes Formular

Abb. 2: Beispielhaftes Formular

Anwendung von One-Time Binding

Die Aufgabe des Digest Cycles ist es, das UI (DOM) und das Datenmodell synchron zu halten. Diese Synchronisation ist allerdings nur für Elemente im Scope notwendig, die sich nach dem ersten Rendern der Seite oder der Komponente noch verändern können. Als Beispiel zeigt Abbildung 2 ein Formular zur Bearbeitung eines Benutzeraccounts. Das Anlegedatum und der Benutzername können nachträglich nicht mehr verändert werden, d. h. diese Felder müssen nach dem ersten Rendern nicht mehr mit dem Datenmodell synchron gehalten werden. Die entsprechenden Watcher können also entfernt werden. Für das Passwort und die E-Mail-Adresse bleiben die Watcher bestehen, um das Bearbeiten auch nach dem ersten Rendern zu ermöglichen.

Dies lässt sich mit „One-Time Binding“ erreichen. Dabei wird die Bindung, das „Binding“, zwischen dem Datenmodell und dem DOM-Element nur solange aufrechterhalten, bis die DOM-Elemente gerendert wurden. Anschließend werden das Binding und damit der Watcher entfernt. One-Time Binding lässt sich auf zweierlei Arten realisieren: über eine AngularJS-native Syntax oder eine Komponente.

Ab Version 1.3. bietet AngularJS eine eigene Syntax für One-Time Binding: {{:varName}}. Der Vorteil dieses Ansatzes ist, dass es sich um eine native Funktionalität von AngularJS handelt. Der Nachteil ist die Beschränkung auf Versionen nach 1.2.x und eine geringe Flexibilität. Beide Nachteile werden von der Komponente bindonce behoben. Die Komponente lässt sich auch für AngularJS-Applikation mit Version 1.2. anwenden und ermöglicht One-Time Binding nicht nur für die Ausgabe von Werten aus dem Datenmodell, sondern ebenso die Veränderung des DOMs. Dafür stellt bindonce Direktiven bereit, die die gleiche Funktionalität bieten wie AngularJS-native Direktiven, aber keine Watcher anlegen. Eine vollständige Liste an Direktiven sowie deren Funktionalität und Verwendung ist auf der Komponentenwebsite zu finden. Diese alternativen Direktiven sind besonders dann hilfreich, wenn das UI abhängig von der Datenmodellausprägung angepasst wird, sich das UI aber nach dem Rendern nicht mehr ändert. Ein typisches Beispiel ist das Anpassen einer Tabelle, bei der bestimmte Spalten über ng-if ein- oder ausgeblendet werden, je nach Belegung des Datenmodells.

Häufig kommt es vor, dass AngularJS das DOM rendert, die anzuzeigenden Daten aber noch nicht verfügbar sind, beispielsweise weil die Serverantwort noch nicht vollständig angekommen ist. In diesem Fall werden die Daten auch dann nicht mehr gerendert, wenn sie dann vom Server eintreffen, weil das Binding von der bindonce-Komponente bereits entfernt wurde. Um diesem Problem vorzubeugen, kann der bindonce-Komponente das Datenobjekt übergeben werden, auf dessen Verfügbarkeit gewartet werden soll, bevor das Binding entfernt wird (Listing 1).

Diese Maßnahme eignet sich für Daten, die sich nach dem Rendern nicht mehr ändern. Auch wenn diese Maßnahme eine zusätzliche AngularJS-Komponente und umfangreiche Anpassungen erfordert, rechtfertigt der Effekt die Aufwände.

<table>
  <!-- Das user-Objekt muss verfügbar sein, bevor das Binding entfernt wird --> 
  <tbody bindonce="user">
    <tr><td bo-text="user.username"></td></tr>
    <tr><td bo-text="user.date"></td></tr>
  </tbody>
  <tbody>
    <tr>
      <td><input ng-model="user.email" /></td>
    </tr>
    <tr>
      <td><input ng-model="user.password" /></td>
    </tr>
  </tbody>
</table>

Ausgabe von Listen mit „ng-repeat“

Für die Ausgabe von Datenlisten bietet AngularJS die ng-repeat-Direktive, die die enthaltenen DOM-Elemente für jeden Listeneintrag wiederholt. Wird diese Direktive wie in vielen Tutorials beschrieben verwendet, kann dies schnell zu einer extrem hohen Anzahl an Watcher-Funktionen führen, wie in Listing 2 (vereinfacht) veranschaulicht.

<table>
  <tr ng-repeat="user in users">
    <td ng-bind="user.username"></td>
    <td ng-bind="user.name"></td>
    <td ng-if="user.role === '1'">Admin</td>
    <td ng-if="user.role < 1">User</td>
    <td ng-bind="user.email | format"></td>
    <td ng-if=”isAdmin”><a>Edit</a></td>
  </tr>
</table>

Die Tabelle aus Listing 2 zeigt eine Liste mit Benutzern, bestehend aus Benutzername, Name, Rolle und E-Mail-Adresse sowie ein Link zum Bearbeitenformular, sofern der eingeloggte Benutzer ein Administrator ist. Da AngularJS für jedes Datum, das über eine native Direktive (ng-*) verarbeitet oder angezeigt wird, einen eigenen Watcher anlegt, werden für jeden Benutzer sechs Watcher-Funktionen angelegt: drei ng-bind- und drei ng-if-Direktiven. Mit dem eingeführten One-Time Binding lässt sich die Anzahl der Watcher auf einen reduzieren – den ngRepeatWatch-Watcher (Listing 3). Bei sehr langen Listen kann zudem infiniteScroll die Responsiveness verbessern.

<table>
  <tbody bindonce ng-repeat="user in users">
    <tr>
      <td bo-text="user.username"></td>
      <td bo-text="user.name"></td>
      <td bo-if="user.role === '1'">Admin</td>
      <td bo-if="user.role < 1">User</td>
      <td bo-bind="user.email | format"></td>
      <td bo-if="isAdmin"><a>Edit</a></td>
    </tr>
  </tbody>
</table>

Bei der Ausgabe von Listen kann One-Time Binding auch dann verwendet werden, wenn sich die Daten noch ändern, indem über ein Array aus Objekten iteriert wird: Bei einer Veränderung der Daten wird das Array neu initialisiert und die Objekte in das Array übertragen. Dadurch verändert sich der einzig gewachte Wert – nämlich im ng-repeat – und es wird eine Aktualisierung angestoßen. Listing 4 verdeutlicht dieses Vorgehen für die Ausgabe der Usertabelle.

Diese Maßnahme eignet sich für die tabellarische Ausgabe von Daten, die sich nach dem Rendern nicht mehr ändern. Sie erfordert zwar eine zusätzliche AngularJS-Komponente und umfangreiche Anpassungen, der Effekt rechtfertigt aber auch hier die Aufwände.

var i, numUsers = serverResp.users.length;
// Das einzige Element, für das ein Watcher existiert, hat sich verändert
$scope.users = [];
// Um sicherzustellen, dass keine veralteten 
// Objekte im Array enthalten sind, werden 
// diese neu in das Array geladen
for(i = 0; i < numUsers; i++) {
   $scope.users[i] = serverRes.users[i];
}

Interpolation vermeiden

AngularJS verwendet Interpolation ($interpolate()-Funktion), um einen String auf Muster zu analysieren, diese zu parsen und mit Werten aus dem Datenmodell zu verknüpfen. Ein typischer Anwendungsfall ist die Ausgabe eines Linkpfads mit /object/{object.id}. Die bindonce-Komponente bietet jeweils zwei Alternativen für die ng-href- und ng-src-Direktiven an: bo-href und bo-href-i bzw. bo-src und bo-src-i. Die mit dem Postfix „i“ gekennzeichneten Direktiven erlauben Interpolation, sollten aber vermieden werden, da jede Interpolation einen weiteren Watcher anlegt (innerhalb der $interpolate()-Funktion). Stattdessen sollten die Direktiven bo-href und bo-src genutzt werden, wobei der Pfad entweder im JavaScript-Code oder über String-Operationen im HTML erstellt werden (Listing 5).

$scope.user = {
   id: 'uuid-39df732459',
   username: 'foo'
};
$scope.user.path = $interpolate('#/user/{{id}}')($scope.user);
<a bo-href="'{{user.path}}">Details</a>
<a bo-href="'/user/' + user.id">Details</a>

Eine weitere Situation, in der Interpolation zum Einsatz kommt, ist die Verwendung der {{}}-Syntax. Hier sollte immer der Direktive Vorzug gegeben werden, da diese zwar einen Watcher hinzufügt, die Ausgabe aber nur bei einer Veränderung der Daten verarbeitet wird. Für die Interpolation von {{}} wird bei jedem Digest Cycle ein Dirty Check durchgeführt, unabhängig von den Veränderungen der Daten. Listing 6 verdeutlicht den Unterschied.

<td>{{user.username}}</td>
<td ng-bind="user.username"></td>
<td>{{'username' | translate}}</td>
<td ng-translate="username"></td>

Hinzu kommt der Vorteil, dass die bindonce-Komponente genutzt werden kann, sofern sich die Daten nach dem Rendern nicht mehr verändern.

Diese Maßnahme eignet sich für jedwede Ausgabe von Daten im UI. Der Aufwand ist vergleichsweise gering und neben der Reduktion der Watcher hat die Verwendung von Direktiven statt {{}} den positiven Nebeneffekt, das im UI keine Artefakte während des Ladens zu sehen sind.

Maßnahmengruppe 2: Dirty State Checks beschleunigen

Die zweite Maßnahmengruppe beschleunigt den Dirty Check auf dem Datenmodell, der mit jedem Digest Cycle durchgeführt wird (Abb. 1, Kreis 2). Die Watch-Funktion kann entweder eine Expression oder eine Funktion übergeben werden, um den Dirty State eines Scope-Wertes zu ermitteln. In beiden Fällen muss darauf geachtet werden, dass die Überprüfung so günstig wie möglich durchgeführt werden kann. Anders formuliert muss der Vergleich des alten Werts, der von AngularJS für den Vergleich vorgehalten wird, und dem aktuellen Wert so einfach wie möglich sein. Neben einer schnelleren Auswertung reduziert dieses Vorgehen auch den Memory Footprint der Anwendung, da die für den Vergleich vorgehaltenen Daten wesentlich kleiner sind.

Keine Watcher für Objekte setzen

Anstatt ein ganzes Objekt zu überprüfen, sollte auf einem Attribut des Objekts gearbeitet werden, idealerweise auf einem primitiven Datentyp. So kann im Beispiel zum Benutzer auf die ID des Benutzers (user.id) anstatt auf das gesamte Userobjekt gewatcht werden (Listing 7).

// Dieser Watcher muss das gesamte Userobjekt betrachten
scope.$watch('user', function(newValue) {
  // Handling des neuen Werts
}, true);
// Dieser Watcher kann sich auf die ID, meist
// ein primitiver Datentyp, beschränken
scope.$watch('user.id', function(newValue) {
  // Handling des neuen Werts
});

Auch wenn AngularJS den semantischen Vergleich von Objekten erlaubt – der dritte Parameter der $watch()-Funktion ist true –, sollte auf diese Funktion verzichtet werden.

Diese einfache Maßnahme hat einen hohen Impact. Zudem erhöht sie die Codequalität, da explizit definiert wird, wann auf eine Änderung im Datenmodell reagiert werden soll.

Check an AngularJS anpassen

AngularJS verwendet eine proprietäre Funktion für den Vergleich von Objekten und Daten, die beim Check des Dirty States zum Einsatz kommt. Mit den Profiling-Werkzeugen der Google Chrome DevTools kann der Geschwindigkeitsunterschied verschiedener Ausdrücke überprüft werden, beispielsweise, ob eine Expression oder eine Funktion definiert wird. Alternativ kann Domänenlogik eingearbeitet werden, d. h. aus der Belegung von Wert A und B kann auf den Dirty State von Wert C geschlossen werden.

Diese Maßnahme ist nur aus Gründen der Vollständigkeit angegeben. Die Performanceanalyse der Vergleichsoperationen ist sehr aufwendig und der erzielte Effekt dennoch gering.

Maßnahmengruppe 3: Die Digest Cycle Trigger reduzieren

Die dritte Maßnahmengruppe reduziert die Ereignisse, die eine neue Ausführung des Digest Cycles anstoßen (Abb. 1, Kreis 3) und damit die Aufrufe der Watcher-Funktionen und die Ausführung der Dirty Checks reduzieren. Beide der nachfolgend erläuterten Maßnahmen beschreiben die angepasste Verwendung von nativen AngularJS-Direktiven.

Anpassungen am UI

AngularJS bietet zwei Ansätze bzw. Direktiven, um das UI entsprechend der Daten im Scope anzupassen: ng-hide/ng-show und ng-if/ng-switch. Die Ansätze gleichen sich in ihrer Funktion, da sie beide Elemente des UI sichtbar machen oder verstecken. Jedoch unterscheiden sich die Ansätze in ihren Auswirkungen auf den Scope: ng-hide/ng-show blendet die DOM-Elemente nur mittels CSS aus, ng-if/ng-switch hingehen verändert das DOM und stößt dadurch eine Digest-Cycle-Ausführung an. Es gibt keine allgemein gültige Aussage, dass einer der beiden Ansätze vorzuziehen wäre. Stattdessen muss abhängig von der jeweiligen Situation entschieden werden. Finden viele Datenveränderungen statt, sollte auf ng-hide/ng-show zurückgegriffen werden, da das Setzen oder Entfernen von CSS-Klassen vergleichsweise günstig ist, das DOM nicht verändert und damit der Digest Cycle nicht ausgeführt wird. Gleichzeitig bedeutet dies aber auch, dass die Daten im Scope bleiben und bei jedem Digest Cycle überprüft werden. Finden wenige Datenveränderungen statt, sollte ng-if/ng-switch verwendet werden, da damit nicht mehr benötigte Elemente aus dem Scope entfernt werden. Das hat einen positiven Einfluss auf den Dirty Check, da weniger Elemente überprüft werden müssen.

Dies ist eine sehr einfache Maßnahme, da nur native AngularJS-Direktiven verwendet werden. Konsequent angewandt verdeutlicht die Unterscheidung außerdem, welches UI-Verhalten zur Entwicklungszeit antizipiert wurde. Zu beachten ist, dass Daten, die nur mit bestimmten Berechtigungen zu sehen sein dürfen, mit ng-if aus dem DOM entfernt werden müssen.

Events über Event Listener und nicht über Angular-Direktiven behandeln

AngularJS bietet mehrere Direktiven zum Handling von Benutzerinteraktionen, beispielsweise ng-klick oder ng-mouseenter. Unter Performancegesichtspunkten hat die Verwendung dieser Direktiven drei Nachteile.

  • Wie im ersten Teil der Artikelserie zu den AngularJS-Konzepten erläutert, wird für jede ng-*-Direktive ein Watcher angelegt, also auch für die Direktiven zum Event Handling.
  • Bei jedem Event wird eine Digest-Cycle-Ausführung angestoßen, unabhängig davon, ob sich die Daten geändert haben oder nicht. Auch wenn dies nur in einem Idle Digest Cycle resultieren würde, ist es doch zusätzlicher Berechnungsaufwand.
  • Der initiierte Digest Cycle überprüft immer den gesamten Scope, anstatt nur den betroffenen Teilbaum. Dies ist darin begründet, dass die $apply()-Funktion auf dem RootScope aufgerufen wird, anstatt die $digest()-Funktion auf dem potenziell betroffenen Teilbaum.

Die drei aufgeführten Nachteile können mit einem eigenen Event Listener auf das DOM-Element vermieden werden, beispielsweise mittels jQuery, das ohnehin in AngularJS enthalten ist. Alternativ gibt es dazu auch Ansätze, die direkt im AngularJS-Core ansetzen. Damit wird der zusätzliche Watcher vermieden, und es kann situativ entschieden werden, ob und wo ein Digest Cycle angestoßen werden soll. Listing 8 zeigt, wie solch ein Event Listener erstellt werden kann. Die Callback-Funktion enthält dabei drei Varianten: Das Datenmodell wird nicht angepasst und das Datenmodell wird angepasst und erfordert einen partiellen oder vollständigen Digest Cycle.

Diese Maßnahme erfordert umfangreiche Anpassungen, die auch die Architektur des Frontends beeinflussen können, beispielsweise bei der Strukturierung von Direktiven. Die gezielte Aktualisierung des Scopes hat dennoch nicht zu vernachlässigende Vorteile.

angular.module('module').directive('openLink', function () {
  return {
    // Nur auf Attributnamen matchen
    restrict: 'A',
    // Keinen Scope angeben, damit wird der 
    // Scope des einbindenden Elements geerbt 
    // (Synonym für scope: false)
    link: function (scope, element, attr) {
      element.on('click', function () {
      // 1) Keine Datenmodellveränderung,
      // daher keinen Digest Cycle triggern
      window.location = attr['open-link'];
      // 2) Datenmodellveränderung und 
      // sicherstellen, dass andere Elemente
      // im Scope nicht betroffen sind
      scope.key = value;
      scope.$parent.$digest();
      // 3) Datenmodellveränderung und
      // den gesamten Scope überprüfen 
      scope.$apply(function(scope) {
         Scope.key = value;
      }
    });
  };
});
<a open-link=“#/path/to/side“>Link</a>

HTTP-Requests zusammenfassen

Jede Serverantwort verändert potenziell das Datenmodell, d. h. wann immer die Promises von $http– bzw. $resource-Objekten auflösen, wird ein Digest Cycle initiiert. AngularJS Promises werden in diesem Artikel aus Platzgründen nicht behandelt, siehe die Dokumentation für eine Erläuterung. Um die Anzahl der Digest Cycles weiter zu reduzieren, kann untersucht werden, ob sich HTTP-Requests reduzieren oder zusammenfassen lassen.

Diese Maßnahme lässt sich jedoch nur in wenigen Situationen sinnvoll einsetzen, insbesondere weil sie Veränderungen am Server oder dessen REST-Interface verlangt und im Gegensatz zum REST-Prinzip steht, dass jedes Datum eindeutig adressierbar sein muss. Auch diese Maßnahme ist mehr aus Gründen der Vollständigkeit angegeben. Die Umsetzung verursacht einen erheblichen Aufwand, da auch das Backend betroffen ist. Zudem werden potenziell allgemeine Designprinzipien verletzt. Der Performancegewinn ist dagegen gering.

Digest Debounce richtig wählen

Da jede Veränderung in einem Eingabefeld auch eine Veränderung im Datenmodell nach sich ziehen kann, wird bei jedem Tastenanschlag in einem Eingabefeld, das mit einem Scope-Element verbunden ist, ein Digest Cycle angestoßen. Dies kann insbesondere während der Eingabe eines Suchbegriffs zu einer wahrnehmbaren Verzögerung führen, da sich die Anzeige mit jedem Tastenschlag verändert.

Um diese Verzögerung zu reduzieren, kann der Digest-Cycle-Aufruf punktuell als eine „Debounce Function“ definiert werden. Eine Debounce-Funktion ist eine Funktion, die ihre Ausführung erst dann beginnt, wenn sie für einen vorgegebenen Zeitraum nicht aufgerufen wurde. Bei dem Beispiel der Suchmaske bedeutet dies, dass die Suche erst dann ausgeführt wird, wenn der Benutzer für ein vorgegebenes Zeitfenster keine Änderung am Suchbegriff mehr vorgenommen hat.

Seit AngularJS 1.3 kann bei einem Eingabefeld neben dem Model-Element auch ein Debounce-Zeitrahmen angegeben werden:

<input ng-model="user.name" ng-model-options="{ debounce: 150 }" />

Der Digest Cycle wird erst angestoßen, wenn der Benutzer mindestens 150 Millisekunden keine Veränderung durchgeführt hat. Für die Wahl des Zeitfensters kann keine allgemeingültige Vorgabe gemacht werden: Wird das Intervall zu kurz gewählt, wird der Digest Cycle möglicherweise zu oft aufgerufen und verlangsamt die Applikation. Ist das Intervall zu lang, werden Änderungen erst zu spät propagiert und die Applikation reagiert träge.

Da AngularJS die Definition eines Debounce-Zeitfensters nativ anbietet, ist der Aufwand der Maßnahme sehr klein, der erreichte Effekt kann bei richtig gewähltem Zeitintervall aber enorm sein.

Maßnahmengruppe 4: Die Digest-Cycle-Ausführung beschleunigen

Die vierte Maßnahmengruppe reduziert die Aktivitäten und Funktionsaufrufe, die während der Ausführung eines Digest Cycles stattfinden, aber nicht mit den Watcher-Funktionen zusammenhängen (Abb. 1, Kreis 4).

Filter-Aufrufe reduzieren

Jeder Filter wird pro Digest-Cycle-Ausführung zweimal aufgerufen, einmal für das Erkennen von Änderungen und ein weiteres Mal um sicherzustellen, dass es seit der letzten Digest-Cycle-Ausführung keine Veränderungen mehr gegeben hat, d. h. dass sich das Datenmodell stabilisiert. Daher sollten die Filter möglichst vor dem Rendering im DOM angewandt werden, insbesondere wenn es um reine Formatierungen der Daten geht. Mit anderen Worten sollten die Daten vollständig aufbereitet sein, bevor sie an die View zum Rendering weitergegeben werden. Listing 9 zeigt die unterschiedliche Verwendung von Filtern, einmal in der View und einmal mit aufbereiteten Daten.

<span>{{user.creationDate | date}}</span>
// Alternativer Filteraufruf im JavaScript
$scope.users = serverResponse;
angular.forEach($scope.users, function(user){
   user.creationDateFormatted =
     $filter(date)(user.creationDate);
});
<span>{{user.creationDateFormatted}}</span>

Die Aufbereitung der Daten im Controller oder der Direktive erzeugt wesentlich weniger Last, da der Filter nur einmal statt in jedem Digest Cycle angewandt wird. Der Implementierungsaufwand der Maßnahme ist zwar vergleichsweise gering, dennoch verletzt sie streng genommen die Trennung von Datenverarbeitung und Darstellung. Der erheblich kleinere Call Tree macht diesen Nachteil aber wett.

Natives JS verwenden

Für die verschiedenen Aufgaben bei der Webcliententwicklung sind in den letzten Jahren hervorragende Frameworks entstanden, die speziell für ein Teilgebiet geeignet sind, beispielsweise jQuery für DOM-Manipulationen und Event Handling. Jedes dieser Frameworks sollte für den angedachten Zweck eingesetzt werden. Dies gilt gerade für AngularJS, das sich auf das Binding zwischen und die Synchronisation von View und Datenmodell spezialisiert hat. Für eine vereinfachte Entwicklung bringt AngularJS zwar auch Funktionalitäten mit, die nicht direkt mit dem Daten-Binding zusammenhängen, beispielsweise Event Listener. Dennoch sollte hier auf spezialisierte Frameworks zurückgegriffen werden, da sie meist die gleiche oder bessere Funktionalität mit weniger Overhead oder geringeren Kosten bereitstellen.

Dieser Ansatz ist jedoch nicht ganz unumstritten, da durch die reine Verwendung von AngularJS zwar vom DOM abstrahiert werden kann, aber weniger abstrahierende bzw. native Funktionalitäten meist wesentlich performanter sind. Da beide Argumentationen ihre Vor- und Nachteile haben, kann eine allgemeine Empfehlung keine nachhaltige Hilfe sein. Dementsprechend ist die Betrachtung dieses Aspekts weniger als Maßnahme, sondern mehr als Hinweis zu verstehen.

Maßnahmengruppe 5: Die Scope-Komplexität reduzieren

Die fünfte und letzte Maßnahmengruppe reduziert die Komplexität des Datenmodells, d. h. des Scopes (Abb. 1, Kreis 5). Je kompakter der Scope ist, umso geringer ist der Memory-Footprint und umso weniger Attribute müssen von AngularJS betrachtet werden. Ein positiver Nebeneffekt ist die verbesserte Verständlichkeit des Codes. Die Scope-Komplexität lässt sich durch eine korrekte Verwendung von Scopes in Direktiven und möglichst kompakten Serverantworten erreichen.

Scopes in Direktiven sinnvoll einsetzen

AngularJS-Direktiven erweitern das HTML-Vokabular um (wiederverwendbare) Komponenten. Für jede dieser Komponenten kann eine von drei Strategien für das Scope Handling definiert werden. Diese Strategien unterscheiden sich darin, ob der Parent Scope mitverwendet, der Parent-Scope prototypisch geerbt oder aber ein eigener, isolierter Scope erstellt wird.

Parent Scope mitverwenden

Wenn der Parent-Scope mitverwendet werden soll, legt die Direktive keinen eigenen Scope an, sondern verwendet den Scope des Parent-Containers ohne Vererbung. Diese Strategie ist besonders dann passend, wenn rein mit statischen Werten gearbeitet werden kann, die bereits zur Linkzeit der Direktive bekannt sind (wie in der openLink-Direktive). In diesem Fall ist kein Aufwand für das Scope Handling notwendig. Es sollte darauf geachtet werden, schreibende Zugriffe auf den Scope sorgfältig zu prüfen, um ungewollte Seiteneffekte im Parent-Container zu vermeiden. Diese Strategie wird mittels scope: false gewählt und stellt den Angular-Standardwert dar.

Parent Scope prototypisch erben

Bei der zweiten Variante legt die Direktive einen neuen Scope an, der prototypisch vom Parent-Container erbt, d. h. diese Direktiven sind kontextsensitiv. Beim schreibenden Zugriff muss darauf geachtet werden, dass bei Objekten der Scope des Parent-Containers verändert wird, bei primitiven Datentypen jedoch ein neuer Wert im Direktiven-Scope erzeugt wird, der den Wert im Parent-Scope verschattet. Für den schreibenden Zugriff auf Objekte gilt das gleiche Gebot wie bei der vorherigen Variante: Es kann leicht zu Wechselwirkungen kommen, die schwierig zu debuggen sind, beispielsweise das versehentliche Überschreiben oder Verschmutzen des Parent Scopes. Diese Strategie wird mittels scope: true gewählt.

Isolierten Scope erstellen

Die Direktive legt bei Strategie Nummer 3 einen neuen, vom Parent Scope isolierten Scope an. Diese Strategie eignet sich besonders für „self-contained“ Komponenten. Über die Scope-Variablen wird eine Art API bereitgestellt. Diese Strategie wird wie in Listing 10 angegeben definiert.

Die Scopes in Direktiven sinnvoll einzusetzen, ist wenig aufwendig und unterstützt gleichzeitig ein sauberes Datenhandling. Der Impact ist ebenfalls hoch, da die Größe und Komplexität des Scope-Baums stark beeinflusst und reduziert werden kann.

scope: {
  // Two-Way Binding zwischen einbindendem 
  // Element und der Direktive; funktioniert
  // nur für Objekte und nicht für primitive
  // Datentypen
  myWritableObject : '=',
  // One-Way Binding für Strings
  myString : '@',
  // One-Way Expression
  myReadableObject : '&'
}

Serverantworten so kompakt wie möglich halten

Moderne Frameworks zur Entwicklung von RESTful Web Services, wie das Spring Framework machen es einfach, Objekte JSON-kodiert an den Client zu senden. Folglich muss der AngularJS-Client Objekte im Scope verwalten, die wesentlich mehr Informationen enthalten und komplexer sind, als auf Clientseite notwendig wären. Die Informationsmenge lässt sich mit „Data Transfer Objects“ (DTO) steuern. Die Einschränkung der zu übertragenen Felder kann zwar auch mit Annotationen wie @JsonIgnore in den Domänenobjekten beeinflusst werden, im Hinblick auf eine saubere Applikationsarchitektur sollte aber direkt auf DTOs zurückgegriffen werden. Dafür werden serverseitig Domänenobjekte in DTOs umgewandelt, die nur einen Teil der Attribute des Domänenobjekts enthalten und ggf. neue Attribute aufweisen, die sich aus mehreren Attributen der Domänenobjekte berechnen. Damit kann die Serverantwort genau an die Anforderungen auf Clientseite abgestimmt werden und das clientseitige Datenmodell ist so detailliert wie nötig, gleichzeitig aber so kompakt wie möglich. Die Notwendigkeit und der Aufwand dieser Maßnahme hängt zu großen Teilen vom Backend der Applikation ab.

Fazit

Besondere Vorsicht ist geboten, wenn native AngularJS-Direktiven (ng-*) zum Einsatz kommen. Da diese den Digest Cycle und die Watcher-Funktionen schnell (sehr) negativ beeinflussen können. Das Gleiche gilt auch für verschiedene Direktiven von Drittanbietern. Der Date Picker aus der UI-Bootstrap-Komponente legt beispielweise pro Instanz ca. 300 Watcher an und widerspricht damit den Zielen der Maßnahmengruppe 1 zur Reduktion der Watcher.

Geschrieben von
Dr. Christian Straube
Dr. Christian Straube
Dr. Christian Straube arbeitet seit seiner Promotion zur kennzahlenbasierten Analyse von IT-Infrastrukturen an der Ludwig-Maximilians-Universität in München bei Zühlke Engineering. Als Projektleiter und Software Engineer modelliert, formalisiert und strukturiert er IT-Probleme, um Kundenprojekte erfolgreich umzusetzen. Erfahren Sie mehr auf seinem Blog.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: