Data Analysis
AngularJS: Performance Optimization with One-time Bindings
If you’ve ever developed a complex single page application using AngularJS (1.x.x), you will have undoubtedly struggled with laggy UI updates. AngularJS’s two-way binding, while it certainly eliminates the need to manually create event listeners to keep the DOM and your javascript state in sync, also creates a significant problem: for every binding, AngularJS is constantly monitoring both the DOM and your $scope for changes, and every digest cycle needs to assess the binding for changes. Even with smallish AngularJS applications, these watchers can easily number in the hundreds or even thousands. This naturally bogs down the browser, which can only handle so many asynchronous processes at a time, causing long delays and a potentially ugly UI while AngularJS scrambles to catch up.
Luckily for us, the maintainers of AngularJS realized this problem and devised a simple solution: one-time data binding via :: (introduced in version 1.3). It’s important that you don’t confuse this with one-way binding. One-way binding via {{..}} means that AngularJS only needs to watch your $scope, but won’t be on the lookout for changes to the value from the DOM. One-way binding is also a useful way to reduce the number of your AngularJS watchers, as it effectively cuts the work in half for a given binding. However, it’s possible to cut it down even further, to have almost no watchers at all!
Quick Tip: You can easily see how many AngularJS bindings are registered by using the AngularJS Watchers Chrome extension. Try to keep your watchers under 1000 for optimal performance and never exceed 2000 at maximum.
What is one-time binding?
At a high-level, one-time data binding means that the template is rendered once based on the current $scope and then it never updates. What’s actually happening under the hood is the creation of a watcher for each expression which will automatically unregister itself when the expression resolves to a value. This means anything but null or undefined, it does not mean truthy. Empty strings, empty arrays, or 0 will successfully unregister the watcher. However, for objects, this means that every property must resolve to a value. The idea is that AngularJS will keep doing its thing while your data is in the process of loading, then once it’s loaded, it’ll stop.
Consider the following two use cases:
- A dialog for editing your user profile
- A list of product search results
In the first case, it makes perfect sense to have two-way data binding. The intent of input elements is literally to have the user interact with it, and your $scope should be aware of changes to their values immediately. This is where AngularJS shines: it saves the effort of either manually adding change listeners to every input or - worse - not doing anything until save, at which point you have to manually retrieve the values for all the inputs. Neither are very attractive solutions. Not to mention, the number of inputs is generally fairly low, so registering 15-20 watchers is reasonable and won’t significantly impact your performance.
However, in the second case, we are mostly just using AngularJS as a templating language. It’s still not static; we do need to dynamically generate the list of search results; but once it’s been loaded, it’s not going to change unless the user initiates another search. Now imagine you are showing 50 search results and each result has 10 properties associated with it - say product name, price, image URL, reviews, etc. Now you’ve created 500 watchers that are doing… nothing whatsoever, except during the relatively rare occurrence of loading a new set of search results. Imagine how many browser resources could be freed if it didn’t have to needlessly keep track of that many watchers.
But… my template will never update!?
It seems like this is not something we’d ever want. After all, isn’t one of the major benefits of using a framework such as AngularJS that you don’t have to manually manage the relationship between the view and controller? If AngularJS stops watching for changes, does that mean you have to go back to the bad old days and manually register event listeners? Even in the second use case, the search results may not update frequently, but it’s entirely likely that we’ll want to update them at some point, without requiring the user to refresh the page. At that point, if we update the $scope with the new search results, AngularJS isn’t watching, and the template will not be updated. We need a way to easily trigger a re-render of one-time bound expressions, that will trigger when and only when we tell it to.
The key to solving this problem is the ng-if directive. Unlike ng-show, ng-if does not render any of its children if its condition resolves to false. If that condition changes to false and the children had already been rendered, they will literally be deleted from the DOM and their associated $scope and controllers will be destroyed. Thus, by necessity, when the condition changes back to true, the children must be rendered again from scratch, including elements with one-time binding. We can leverage this to get the best of both worlds: limited watchers (and thus improved performance) and automatic DOM updates.
All you need to achieve this balance is a single two-way bound $scope variable that can be used to gate our one-time bound content. The ng-if serves as our trigger for re-rendering. In this example use case, that could be something like $scope.isLoading. When the user clicks the search button, you set $scope.isLoading = true, then set it back to false when the request completes and you’ve loaded the new results into the $scope. Then in our template, we have something like:
It’s almost like magic! The above template has only one watcher! The watcher on the ng-if conditional is the only one we actually need. The individual expressions for each search result change relatively rarely. And when they do change, they don’t change independently of each other. We’re not going to want to update only the title for a single result, we’re going to want to replace the old search results with the new in one fell sweep.
That’s the beauty of this solution. By using a simple flag to gate the results, we’re effectively telling AngularJS only to update exactly when we want it to, in a logical, easy-to-follow way. It’s clear by looking at the HTML what the purpose of the conditional is and we’re not messing with complicated event listeners or custom watchers. It’s definitely a win-win situation.
Keep in mind as well that this doesn’t have to be limited to entire pages of search results; this approach can be leveraged for small chunks of your application. You can control how many watchers you have and limit them to the ones you actually need. A lot of small optimizations can build into a large overall performance boost.
Strategies for optimizing with one-time binding
1) Avoid using functions in your template
One-time binding by nature does not work with functions, only expressions. If you attempt to put a one-time binding on a function, it will never unregister, even if the function has returned a non-null value.
If you need to compute properties, it’s considered a best practice in MVC applications to do so in your controller rather than using functions in the template. Often this approach is used in lieu of one-time bindings, as a way of waiting for dependent $scope variables to become available and thus avoid manually registered watchers on the parent $scope that trigger the calculation.
A cleaner strategy is to use a directive that will only render if all its dependent data exists. Take the product rating example again. We’d want to create a directive for displaying review stars and then calculate from a raw rating number (e.g. 3.72342) into a number of stars. We can again leverage ng-if for optimization here.
Bad
Good
2) Don’t rely on falsy values
Because the watcher will only be unregistered when the expression resolves to a non-null value, it’s risky to rely on falsy values. In many cases, being falsy is a perfectly acceptable final state and is not due to waiting for data to load. Take our example above of product search results that contain a rating. Some items may be new and not have any ratings. If the rating property is undefined, it’s always going to be undefined, so we’ll end up with a perpetual watcher if we bind to the raw property.
For conditionals, make sure to use type coercion so that the expression resolves to a boolean. For other expressions, simply wrap it in a conditional that does type coercion, or make sure to default the value.
Bad
Good
3) Use caution when using one-time binding with objects
One-time binding for objects works differently than for primitives or arrays. It’s very easy to accidentally create watchers on objects with falsy properties. Because the watcher is registered for the object as a whole, the condition for unregistering the watcher is much more intelligent than just checking if the object exists. It will only happen if all the properties exist. Without this additional handling, one-time data binding would be virtually useless on objects if the watcher was unregistered as soon as its created, but all its properties haven’t been set yet. On the other hand, it’s much easier to have an undefined property without realizing it.
If possible, try to one-time bind against a primitive rather than an object, and if you need an object, keep it as minimal as possible and ensure that the properties will always be defined.
Bad
Good
Conclusion
I hope you enjoyed this article and learned some helpful strategies for optimizing your AngularJS application! One-time binding, when used correctly, can be a powerful tool for not only optimizing your performance but also simplifying your application by removing the need for complex manual watchers. Go forth and bind!
References
https://docs.angularjs.org/guide/expression#one-time-binding