Like we saw in the previous post, AngularJS uses watchers to detect changes, allowing it to update views as needed. Angular will create a watcher for every expression that we add to our templates using data-bindings, ng-repeat
, or similar directives.
As we learned, each time a watcher is created Angular will add the expression to a watch list, which is then iterated during each digest cycle to evaluate every expression and detect changes. That means, the more watchers are registered, the more Angular has to process during the digest cycle.
In pages that have many components—such as long lists and grids—the amount of watchers can be very high, which can negatively affect the performance of our app and make it feel unresponsive.
In this post we will take a look at two techniques that can help us mitigate this problem and speed up our applications.
One-Time Bindings
One of the most convenient techniques that we have to reduce the amount of watchers is the use of one-time bindings. Any expression that starts with ::
is considered a one-time expression.
<div ng-repeat="user in ::users">
<h1>{{{ ::user.name }}</h1>
</div>
Angular will remove a one-time expression from the watch list once it has been resolved, unlike normal expressions which are evaluated on every digest cycle.
If the expression will not change once set, it is a candidate for one-time binding. For example, internationalization 🇬🇧 🇪🇸
As a result, we have less expressions being watched, which makes the digest loop faster, increases the responsiveness of the app, and allows more information to be displayed at the same time.
Using one-time bindings is an easy and effective way to reduce the amount of watchers, but there's a catch. Angular won't detect changes on each digest cycle and update the view, which makes them only suitable for values that won't change.
Recompiling with Events
What about expressions with a value that might change, yet remain the same most of the time? It's a waste to evaluate them on every digest cycle, but we can't just use one-time expressions since the value might eventually change.
A technique that I have been using in pages where performance is critical is event-driven recompilation.
<h1 recompile-on="user:changed">{{ ::user.name }}</h1>
This technique has three key aspects: one-time expressions, compilation, and event propagation. Let's see how the recompileOn
directive could be written:
# Public: Recompiles an element if an event occurs.
#
# NOTE: Do not use in combination with ng-if or ng-repeat, unless a one-time
# binding is used in the expression.
app.directive 'recompileOn', ($compile) ->
directive =
scope: true
priority: 5
restrict: 'A'
compile: (element) ->
html = element[0].outerHTML
(scope, element, attrs) ->
# Internal: Will trigger a recompilation if the event is triggered.
recompileOnEvent = (eventName) ->
scope.$on eventName, (e) ->
# Remove the previously added listener, if any.
removeListener?()
# Replace the element after the digest loop that triggered the event has ended.
scope.$evalAsync ->
newEl = $compile(html)(scope.$parent)
element.replaceWith(newEl)
# Destroy the old scope, since a new one was created by using compile.
scope.$destroy()
removeListener = recompileOnEvent(attrs.recompileOn)
The directive will listen for a particular event on the current scope, and force a recompilation of the element when the event is triggered. This means that every directive inside the element will be processed from scratch, including our one-time expressions.
Events can be triggered as usual:
$scope.$broadcast('user:changed')
Conclusion
One-time bindings provide a very efficient way to render dynamic values once without taking up watchers, while $on
and $compile
provide a helpful way to render from scratch when we need to. This combination works very well for complex pages where the amount of watchers is taking a toll on performance.
There are many variations that we can introduce to the implementation of recompileOn
, such as allowing to pass several event names, or checking the event arguments for a specific value, such as an id. We can take this idea as far as we want to 🚀
As with any optimization, it's important to consider whether the performance improvement justifies the additional complexity. Use it wisely!