Filters in Angular massively contribute to slow performance, so let’s adopt a sensible way of doing things, which may take you an additional ten minute to code, but will dramatically enhance your application’s performance.
Let’s look at how removing filters in the DOM actually impacts our $digest
cycles.
Table of contents
What’s a DOM filter?
A DOM filter is where we use a |
pipe inside an expression to filter data:
<p>{{ foo | uppercase }}</p>
This might output a String 'todd'
as 'TODD'
as we’ve used the | uppercase
filter. If you’re not well versed with filters, check out my article on custom Filters.
Why are DOM filters bad?
They’re easy to use, which means they probably have some internal performance overhead. And that’s true, filters in the DOM are slower than running filters in JavaScript, however the main concern here is how often DOM filters get run.
Take this example, it uses a DOM filter on an ng-repeat
, type some letters to see how it filters:
This looks great, and has no immediate performance concerns, no page lag or input delay. However let’s dig a little deeper.
Filter $digest evaluation
For us Angular developers, DOM filters are branded as the “awesomeness” that ships with the core, and by all means, this is tremendous, but there’s actually very little about the performance impacts of filters.
Are you ready to realise how your $$watchers
are being choked by using DOM filters? Okay here goes.
Let’s setup a basic test filter:
function testFilter() {
var filterCount = 0;
return function (values) {
return values.map(function (value) {
filterCount++;
// don't do this, this is just a hack to inject
// the filter count into the DOM
// without forcing another $digest
document.querySelector('.filterCount').innerHTML = (
'Filter count: ' + filterCount
);
return value;
});
};
}
angular
.module('app', [])
.filter('testFilter', testFilter);
This majestic testFilter
will be bound to an ng-repeat
, and each time the filter is called, it’ll increment its internal counter and log it out in the console for us.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
We can add it to the ng-repeat
like so:
<ul>
<li>
{{ user.name }}
</li>
</ul>
Let’s also add some data to a Controller and force a $digest
every ~1000ms
to see how quickly these filters start stacking up.
function UserCtrl($interval) {
var users = [{
name: 'Todd Motto'
},{
name: 'Ryan Clark'
},{
name: 'James Ryddyk'
},{
name: 'Jurgen Van de Moere'
},{
name: 'Jilles Soeters'
}];
// force a $digest every ~1000ms
$interval(function () {}, 1000);
this.filteredUsers = users;
}
And the output (note the “Filter count: X” text in the code embed). Please note, the <input ng-model="">
functionality no longer exists however has an ng-model
attribute bound, this is merely just to show filter evaluations inside the $digest
loop.
Boom boom boom. Five filter calls every $digest
loop. This can’t be very efficient right? Especially with an ng-repeat
with 1000
items inside. Think about it.
What’s more, typing something inside the <input ng-model="">
runs a $digest
and numbers soar into double and triple figures. By the time you’ve read down to this stage the figure is probably into the hundreds, if not thousands.
Why are our filters running when we’re not even filtering the ng-repeat
with the ng-model
anymore?
The filters here are run every single $digest
.
$filter in Controller
We almost certainly don’t want the desired behaviour above, which if you’ve got large collections inside an ng-repeat
and DOM filters, this is going to grind your application’s performance to a halt very quickly.
Let’s look at implementing a filter in the Controller using $filter
.
function UserCtrl($filter, $interval) {
var users = [...];
// force a $digest every ~1000ms
$interval(function () {}, 1000);
// updateUsers will call `testFilter` ourselves
this.updateUsers = function (username) {
this.filteredUsers = $filter('testFilter')(users);
};
this.filteredUsers = users;
}
And bind the this.updateUsers
method to the <input ng-model="">
as an ng-change
event:
<input
type="text"
placeholder="Type to filter"
ng-model="username"
ng-change="user.updateUsers(username);">
Yes, we’re still forcing a $digest
every ~1000ms
in this example. Let’s observe the output and see when our ng-repeat
filter gets called…
Answer: no filters are run until you type, rather than every $digest
. We’re making serious progress. Our filter is only called when we activate it through the this.updateUsers
function, it’s not cycled through each $digest
.
Okay that’s great, but how can I run my filter so it’ll work?
Currently the this.updateUsers
method takes an argument (username)
, we can pass this into Angular’s generic (and so wonderfully named) filter
filter:
this.updateUsers = function (username) {
this.filteredUsers = $filter('filter')(users, username);
};
And that’s all we need. Now we have a $filter
that is actually only called when we need to filter, rather than having the filter function run every single $digest
loop. This will significantly reduce the $digest
overhead when not filtering, and run filters when actually necessary.
“Chaining filters”
Yes, chaining filters is great, but their order of declaration affects the output of the filtered collection, and two, you’re going to make the above $digest
filter issues even worse.
How can I chain filters inside a Controller?
Easy, it’s not essentially chaining, filters are just function calls. Filter your first collection, and pass the filtered collection off to another filter, then assign that finalised collection to the public property:
this.updateUsers = function (username) {
var filtered = $filter('filter')(users, username);
filtered = $filter('orderBy')(filtered, 'name');
this.filteredUsers = filtered;
};
With this example, I can use ng-init="user.updateUsers(username);"
to instantly show you how this works:
The filter runs $filter('filter')(users, username)
followed by $filter('orderBy')(filtered, 'name');
. Which passes in var filtered...
as a variable. Essentially it’s doing this inside itself, passing a filter into a filter:
this.updateUsers = function (username) {
this.filteredUsers = $filter('orderBy')($filter('filter')(users, username), 'name');
};
Filters in Controllers: do it.