Using Angular $compile to Escape Watcher Hell
This is a blog on how to handle repeating directive elements without using the ng-repeat directive when using AngularJS. We’ll keep this blog high level and assume the reader knows Angular development.
Sample:
<div ng-repeat='spell in spells'>
<harry-potter doing='spell.name'>
{{spell.incantation}}
</harry-potter>
</div>
This is a blog on watcher
performance and so lets take the sample above and see what happens. If the sample array spells
has 7 items, ngRepeat
creates (7 * 2) + 1 = 15 $watchers
. This is because Angular creates watchers in its built in ng-repeat
directive to support the scope which in this case is the harry-potter
directive. But wait 15 watchers is fine! No need to worry!
Yes, while this is fine for small applications, it quickly digresses into an unmanageable overhead in larger apps. This is especially true when ngRepeating
on directives who use ngRepeat
inside themselves. It is a good rule of thumb to keep your watchers
below 2,000. (as sourced from here, here, and here). Once you start getting into displaying large amounts of data such as fintech tables, commercial customers, or complex transactions; you start hitting that 2,000 watcher mark awfully quickly if you don’t manage your ng-repeats
.
The problems with ng-repeat
:
- Repainting the DOM and Dirty Checking
- The number of [watchers] it initializes just blows up
- Expensive to update with dynamic children
For problem 1, the common thing said is to avoid filtering the ng-repeat
as this causes a re-paint and re-digest of the content each time. More info on Watch Digest here. Another key thing to do is to use track by which was introduced in Angular 1.2 which improves rendering performance.
track by:
<div ng-repeat='spell in spells track by spell.id'>
<harry-potter doing='spell.name'>
{{spell.incantation}}
</harry-potter>
</div>
For problem 2, the root fo the problem is two-way binding, Angular then creates watchers for each scope item it uses. If you could imagine the directive harry-potter
to have another ng-repeat inside of it, you would exponentially grow the number of watchers you have. There are some factors to limit the growth of the watchers (which can arise from ngIfs, any expression in your template, or even the plain old $scope.$watch) by using bind once. Angular 1.3 allows you to specify which scope items are only needed to be binded once (using the double colon ::
marker). Items which are binded once means that it won’t be two way binded. If you are using an Angular version before 1.3, there is a very famous project by Pasvaz to handle this called bindonce.
bindonce:
<div ng-repeat='spell in ::spells track by spell.id'>
<harry-potter doing='::spell.name'>
{{::spell.incantation}}
</harry-potter>
</div>
For problem 3, comes the hard part of the story what do you do when you only wish to programatically add new directives in your ng-repeat without hitting the digest loop for all the other elements that are already there. The trick here is to use $compile coupled with angular.element
. Basically we are manually creating the new directives as if we were doing the ng-repeat
ourselves similar to dynamic children in React.
$compile and angular.element:
function link(scope, iElem, attrs) {
scope.spells.forEach((spell) => {
// Create a Harry Potter directive in our view
const hp = angular.element('<harry-potter doing="spell.name">{{spell.incantation}}</harry-potter>');
// Compile view into a function
const compiledElement = $compile(hp);
// Bind view to scope
const bindedElement = compiledElement(scope);
// Attach to ng-Repeat
iElem.append(bindedElement);
});
}
There you have it, a hack away from making ng-repeat
magical again. Angular really provides all the hooks albeit not all the documentation to support all uses cases of any application. This blog was based off a real world example but have been changed to preserve privacy.