2. Introduction
Why are we here?
AngularJS behave very well on light application
Problems appears when DOM become complex
Our use case : Ability
• Document management system
• <200 documents in DOM : no problem
• >350 documents in DOM : barely unusable
4. The $digestive track
When you bind a value to an element in Angular using
• ng-model,
• ng-repeat,
• {{foo}},
Angular creates a $watch on that value.
--angularjs.org
All watched elements are added to a watch list
When something changes on a scope (e.g model
changes, ng-click…), all watches
are triggered
5. When you watch too much
8-)
It is generally accepted that once the number of watchers reaches 2000+, an
application will start to suffer. AngularJS.org
“You can't really show more than about 2000 pieces of information to a human on a
single page. Anything more than that is really bad UI, and humans can't process
this anyway.”
-- Misko @ StackOverflow
Even more watches: ngClass, ngHide, ngIf, ngShow, ngStyle
6. Even more watches - ng-
repeat
ngRepeat directive instantiates a
template once per item [...] each
template instance gets its own
scope, where the given loop
variable is set to the current
collection item, and `$index` is set
to the item index or key. --
docs.angularjs.org
7. Even more watch - Filters
A convenient way to reduce the number of watchers is to limit their number by
filtering
An AngularJS filter in your HTML will execute (multiple times!) for every digest
cycle[4]
This means that the filtering function, or the filter will execute multiple times, even if
the underlying data or conditions have not changed.
Optimize
• by triggering your filter in your controller instead of having it directly in your
HTML.
• by injecting the filter service in application and if XXX is the service
XXXFilter
• showcase Ability filter by chapter
8. Show Case
Showcase how the number of watches affect
performance
• http://plnkr.co/edit/jwrHVb?p=preview
Showcase how the filters affects performance
• http://jsfiddle.net/m2jak8c8/
Showcase how to write a manually triggered filter
10. Part II
Best practices
General principle
• Limit the cost of digest cycle
• E.g : if a scroll triggers a digest at each frame
• Objective 60 FPS => $digest time < 16ms
Strategies
• Digest only when you need
• Limit number of watchers
• One way binding
• Remove watchers dynamically
• Watch only what is needed
• Make sure what is watched is cheap
• Don’t use ng-mousenter, mouseleave…
• Avoid watch invisible elements
• …
11. Digest only on the scopes
you need
Third party plugins make model changes
“outside” AngularJS
Its’s where $scope.$apply become
handy
However $scope.$apply=~$rootScope.
$digest
Instead we can use $scope.$digest,
which run digest only in the current
scope (and child)
Show case:
http://jsfiddle.net/fqbsdqub/
12. Limit number of watch : one way
binding
Show a variable without attaching a watch to it
Part of Angular since v1.3 before that bindonce library
• Use :: to enable one way binding
Example:
<div ng-repeat="stock in ::ctrl.stocks">{{::stock.name}}</div>
Showcase
http://plnkr.co/edit/jwrHVb?p=preview
13. Use compile over link in
directive- when possible
When a directive appears inside of a repeater :
• compile is called only once
• link and the constructor are called once per iteration
The link function is called as much times as there are cloned directives.
When creating directives, try to get as much work done as possible in the
compile step
Best practice
• Any operation which can be shared among the instance of directives
should be moved to the compile function for performance reasons. --
angularjs docs
SHOWCASE
https://jsfiddle.net/83M34/42/ (look at the console log)
14. Removing the Unneeded
Watches
To disable the watch just call the returned function:
myWatch();
Same process for $timeOut
These watchers are removed when $scope.$destroy()
var myWatch = $scope.$watch('someVariable',
function(newVal, oldVal) {
if (newVal === oldVal) { return; }
});
15. Removing the Unneeded
Watches-2
In the case of $rootScope.$watch, watchers are not
removed
You have to call explicitly on $scope.$destroy
otherwise they will stay
var myWatch = $rootScope.$watch('someVariable',
function(newVal, oldVal) {
if (newVal === oldVal) { return; }
});
16. $watch only what is needed
Sometimes a deep $watch is needed, but not for the entire object you can
watch individual properties.
By stripping out irrelevant data, we can make the comparison much faster
$watch(someObject.someArray.length) vs $watch(someObject.someArray) or even
$watch(someObject) - deep watch
In angular.js 1.1.4: watchCollections
• http://bennadel.github.io/JavaScript-Demos/demos/watch-vs-watch-collection/
.
$scope.$watch(‘bigObject’,myHandler, true);
// better
$scope.$watch(function($scope) {
return $scope.bigObject.foo.
fieldICareAbout;.
}, myHandler, true);
17. Make sure what is being
checked is cheap
With frequent dirty checking, its inadvisable to place calls to complex
functions into Angular expressions
An ng-click somewhere could trigger a lot of dirty checking.
Precompute everything (don't do the computations and expressions in
watches )
• {{ computeTotal(data) }} - this function will be called in each
digest! Precompute the result, cache it, or make it static
18. Don't use ng-mouseenter,
ng-mouseleave, etc.
Using ng-mouseenter/ng-mouseleave triggers the digest cycle
• In general avoid directive that are triggered very often
Use CSS/ vanillaJS wherever you can to avoid this trigger
“Using the built-in directives like ng-mouseenter AngularJS caused our view to
flicker. The browser frame rate was mostly below 30 frames per second. Using
pure jQuery to create animations and hover-effects solved this problem”
Showcase: http://plnkr.co/edit/sFsuqvPajnw1x2LahX7T?p=preview
19. Avoid watching invisible
elements - Use ng-if, not ng-
show
ngShow/ngHide
• “The element is shown or hidden by removing or adding the `.ng-hide` CSS class
onto the element. The `.ng-hide` CSS class is predefined in AngularJS and sets the
display style to none (using an !important flag).” - ngShowHide.js Source Code
ngIf
• one of the best features to come out of Angular 1.2
• If the expression evaluates to a false value then the element is removed from the
DOM,
• otherwise a clone of the element is reinserted into the DOM.
• when an element is removed using `ngIf` its scope is destroyed and a new scope is
created when the element is restored. pros: watches are destroyed too :)
• The scope created within `ngIf` inherits from its parent scope using [prototypal
inheritance]
cons: performance hit as it fully recompiles the angularjs template when hidden/shown
20. Evaluate Only When
slyEvaluateOnlyWhen – slyPreventEvaluationWhenHidden
• Directive for preventing all bound expressions in the current element and its children from
being evaluated unless the specified expression evaluates to a different object.
• Prevent evaluation if the current element is hidden
a alternative for ngIf - you see the DOM but the watches are inactive
can be used together with element visible to optimize watches
21. Prevent deep filtering
Same as for watches
filter on specific fields (prevent deep filtering)
use cheap statements inside filters functions that execute fast or break the
function on the beginning if the condition is not valid
angular.module('MyFilters', [])
.filter('customfiler', function() {
return function(input) {
if (input.age < 18) return
input;
if (intensiveChecking(input))
transformInput(input);
else
return input;
};
});
angular.module('MyFilters', [])
.filter('customfiler', function() {
return function(input) {
if (intensiveChecking(input) &&
input.age > 18)
{
transformInput(input);
} else return input;
};
});
22. Optimizing ng-repeat:
The build-in tools
Track-by
• `item in items track by item.id
• In older Angular
• Uses DOM caching / reuse - less DOM manipulations
• Now :
• “`item in items` is equivalent to `item in items track by $id(item)`” -- ngRepeat.js Source
Code(26.02.2015.)
• $id(item) can be costly, if server provides uid, better use it
Limit to
• `ng-repeat=“item in items limitTo index”
• Limit maximum number of visible items
• Can be used to create a lazy load scroll
23. Direct DOM manipulation
with vanillaJS/JQuery
Don’t do it if there is another way
Pros:
• Fast
• No watches
• Needs a lot of engineering :)
Cons:
• Can’t use the power of angular anymore
• ugly code
24. Optimizing ng-repeat :
Remove non-visible
elements
Pagination
• Simple method to reduce the number of watches for large item sets -
together with DOM caching / reuse its a powerful tool
Virtual Scroll - Angular-vs-Repeat
• PROS:
• VERY easy to use just add <div vs-repeat> above your ng-repeat and enjoy the performance boost
• CONS:
• Sometimes it works perfectly with simple item lists but as soon as the case is more complex a lot of
customization is needed and there is a great possibility that it will flicker (container scrolls and co)
• Problems with nested repeats - can be buggy
• Not easy to combine it it with lazy loading/infinitive scroll
ShowCase
• http://kamilkp.github.io/angular-vs-repeat/#?tab=8
26. Open question : Use ng-bind
instead of {{}}
Some debate but no clear answer on SO
• http://stackoverflow.com/questions/16125872/why-ng-bind-is-better-than-in-angular
Jsperf seems to say {{}} is better
• https://jsperf.com/angular-bind-vs-brackets
What is your opinion?
27. Measures
To measure the time a list rendering takes an directive could be
used which logs the time by using the ng-repeat property
“$last”Manually - track by and limitTo
// Post repeat directive for logging the rendering time
angular.module('siApp.services').directive('postRepeatDirective',
['$timeout', '$log', 'TimeTracker',
function($timeout, $log, TimeTracker) {
return function(scope, element, attrs) {
if (scope.$last){
$timeout(function(){
var timeFinishedLoadingList = TimeTracker.reviewListLoaded();
var ref = new Date(timeFinishedLoadingList);
var end = new Date();
$log.debug("## DOM rendering list took: " + (end - ref) + " ms");
});
}
};
}
]);
<tr ng-repeat="item in items" post-repeat-directive>…</tr>
28. Measures
Number of watches
Digest delay
Monitoring the digest circle lag/delay time of the rootScope or
the selected scope
var vScope = $0 || document.querySelector('[ng-app]');
angular.element(vScope).injector().invoke(function($rootScope
) {
var a = performance.now();
$rootScope.$apply();
console.log(performance.now()-a);
})
30. FUTURE
“Dirty checking can have performance issues, but the core
team can/will start using Object.observe as ES6/harmony
matures. As JS grows, a lot of Angular downsides will stop
being relevant” -- Some guy on Reddit a year ago(2014)
Angular.js 2.0
• WatchTower.js
• Object.observe
31. Questions
What is the blend between Performance optimization and
breaking Angular.js ?
Overuse of optimizations - using Javascript/JQuery to handle
the view directly ?
Is Angular.js really good for every application ?
....
32. REFERENCES
[1] Counting the number of watchers on a page in angular.js
[2] Databinding in AngularJS
[3] Optimizing AngularJS: 1200ms to 35ms, Steven Czerwinksi
[4] Optimizing ng-repeat in AngularJS
[5,7] Optimizing a Large AngularJS Application,Karl Seamon, ng-conf
[6] Improving Angular Dirty Checking Performance Doug Turnbull — April 24,
2014
[8] The Digest Loop and $apply, ngBook
[9] Supercharge AngularJS Performance Measurement and Tuning Sebastian
Fröstl && Damien Klinnert
[10] Speeding up AngularJS apps with simple optimizations, Todd Motto, Aug 6,
2014
[11] AngularJS Performance Tuning for Long Lists, 2013 Sebastian Fröstl,
September 10
Editor's Notes
When you apply, the digest loop will be trigered with all elements of the watch list being processed
Then when everything is done a new digest is performed to check that the performance are stable
The problem is that it is quite easy to create lot of watches, lot of directive actually creating watch
If you mix this with ng-repeat you can easily end up with more than 2000 watches
If an element have 5 watch and is repeated 10 times, then you will end with 50 watches
Filter will reduce the number of watches since elements are not created