January 22, 2015
AngularJS Performace durch One-Time-Binding verbessern
AngularJS 1.3 wurde im Oktober 2014 veröffentlicht und bringt neben zahlreichen Bugfixes auch neue Features. Als Feature bezeichnen die Angular JS Entwickler auch das fallenlassen der Unterstützung des Internet Explorer 8. Das Feature auf das sich dieser Artikel bezieht wurde mit der Version 1.3.0-beta.16 hinzugefügt.
Überblick über die Features in Angular 1.3
- keine IE8 Unterstützung
- ngMessages
- $watchGroup
- strict-DI
- One-Time-Binding
Dieser Artikel fokussiert sich auf den letzen Punkt, das One-Time-Binding, das oft auch als BindOnce bezeichnet wird.
Das Problem
AngularJS bietet ein großartiges Data-Binding System, das es Entwicklern auf einfachste Weise ermöglicht Daten aus Controllern mit der UI zu verknüpfen. Das funktioniert mit kleinen Datenmengen einwandfrei und ohne Kompromisse. Müssen jedoch größere Datenmengen mit der UI verknüpft werden kann es zu Performance Problemen kommen, siehe hierzu das Beispiel 1. Es zeigt eine Tabelle mit ~2.500 Einträge mit Informationen über Städte (Städtenamen, Postleitzahlen und weiteren Informationen). Über der Tabelle ist ein Textfeld zu sehen. Wird das Textfeld fokussiert und eine beliebige Taste dauerhaft gedrückt ist zu erkennen, dass die UI einfriert und eine deutlich wahrnehmbare Verzögerung entsteht.
Beispiel 1: Performance Issue in AngularJS 1.3
Die Ursache
In dem Beispiel 1 werden die Daten mit doppelten geschweiften Klammern an die UI gebunden, ebenso könnte auch die ngBind-Direktive genutzt werden. Zu Demonstrationszwecken werden sowohl Funktionen, wie auch Filter eingesetzt.
1 |
{{zip.city | titleCase}} |
1 |
{{zip._id}} |
1 |
{{zip.pop | number}} |
1 |
{{isHugeCity(zip.pop) ? 'Yes' : 'No'}} |
AngularJS erzeugt intern für jedes Binding einen Watcher, der die Property auf Änderungen überwacht und die Darstellung bei einer etwaigen Änderung aktualisiert. Hierzu bedient sich Angular einer Technik, die als Dirty-Checking bezeichnet wird. Der Dirty-Check ist simple Funktion und hat die Aufgabe Änderungen an einer Property festzustellen. Er wird in der Digest-Phase für jeden einzelnen Watcher durchgeführt und es wird der Callback eines Watchers getriggert wenn eine Änderung an einer Property festgestellt wurde. Es gilt der Grundsatz, dass eine Angular Anwendung ab ~2.000 Watchern¹
inperformant wird, da jeder Watcher in jedem Angular-internen $digest-Zyklus überprüft werden muss. Wie in Beispiel 1 zu sehen ist, hat Angular ~18.000 Watcher erzeugt, die dafür verantwortlich sind, dass die UI einfriert.
Die Lösung
Die Lösung des zuvor beschriebene Problems ist ebenso simpel wie effektiv. Das Angular Team hat sich auf eine nette Syntax geeinigt. So kann eine Angular Expression mit einem Doppelten Doppelpunkt geprefixt werden um den Watcher nach einmaliger Evaluierung zu entfernen, siehe hierzu Beispiel 2. Wenn nun das Textfeld fokussiert und eine beliebige Taste dauerhaft gedrückt wird, ist kein einfrieren der UI zu erkennen.
Beispiel 2: AngularJS 1.3 mit BindOnce
Neue Syntax
Angular Binding mit Watcher: {{zip.city | titleCase}}
Angular Binding ohne Watcher: {{::zip.city | titleCase}}
Wie zu sehen ist genügt es den Code dahingehend anzupassen, dass ::
vor die Expression zu schreiben ist. Angular evaluiert die Expression einmalig und entfernt anschließend den Watcher. Zu beachten ist jedoch, dass eine Änderung des Datenmodells keine Aktualisierung der UI nach sich zieht, da die Properties nun nicht mehr überwacht werden.
Alternative
Da dieses Feature erst ab der Angular Version 1.3 verfügbar ist, muss für eine Angular Anwendung die eine Version vor 1.3 verwendet auf ein Modul aus der Community zurückgreifen. Das bindonce-Modul von Pasquale Vazzana ist auf GitHub https://github.com/Pasvaz/bindonce inklusive einer umfassenden Dokumentation zu finden. Beispiel 3 zeigt abschließend die Verwendung des BindOnce-Moduls mit der Angular Version 1.2
bindonce
Direktive nutzen.Beispiel 3: AngularJS 1.2 mit externem BindOnce-Modul²
Further Reading
- ¹ Databinding in AngularJS: http://stackoverflow.com/a/18381836
- ² Pasquale Vazzana BindOnce-Modul: https://github.com/Pasvaz/bindonce
- Beispiel 1: Angular 1.3 ohne BindOnce
- Beispiel 2: Angular 1.3 mit BindOnce
- Beispiel 3: Angular 1.2 mit externem BindOnce Modul