optimizing a large angular application (ng conf)
TRANSCRIPT
Optimizing a Large AngularJS Application
Karl SeamonSenior Software Engineer, Google
Occasional contributor to AngularJS
Topics
● The Problems● Basics and Best Practices● Diagnosing performance problems● Improving performance within AngularJS
The Problems: What can make an Angular app slow?
● Anything that could affect a normal JS appo Slow DOMo Single-threadedo etc
● Inefficient directiveso link vs compileo $parse vs $eval vs interpolation
● Dirty checkingo Lots of watcherso Slow watcherso Too many calls to $apply
What does slow mean?
● $apply > 25ms● Click handler > 100ms● Show a new page > 1s
o > 10s -> users will give upo 200ms or less is ideal
Directives: compile, link, and constructor
● When a directive appears inside of a repeatero compile is called only onceo link and the constructor are called once per iteration
● When creating directives, try to get as much work done as possible in the compile step (or even earlier, in the factory)
● Exampleo https://github.com/angular/angular.js/commit/f3a796
e522afdbd3b640d14426edb2fbfab463c5
Directives: Transclusion
● For directives that wrap other content● Allows your directive to $digest its own
scope without causing dirty checks on bindings in the user-provided contents
$digest and $apply
● Angular’s documentation favors $applyo Simpler to use $apply all the timeo $apply has special error handling that $digest lacks
● So what’s $digest for?o $apply = $rootScope.$digest + some other stuffo If you update a child scope s, you can call s.
$digest to dirty-check only that s and its descendants
Watchers
scope.$watch(valueExpression, changeExpression, ...)
valueExpression will be executed many times - Make sure it is fast! Avoid touching the dom (_.debounce can help).
$watch and $watchCollection, con’t
● $watch has two comparison modeso referential (default) - quick, shallow comparisono deep - slow, recurses through objects for a deep
comparison; also makes a deep copy of the watched value each time it changes
● Avoid deep $watch whenever possible
$watchCollection
● new in 1.2, used by ng-repeat● Goes one level deep into the watched array
or object● A nice alternative to deep $watch in many
cases
● $interpolate: returns function that evals “a string {{like}} this”
● $parse: returns function that evals “an.expression”
● $scope.$eval: evaluates “an.expression” (using $parse)
$eval, $parse, and $interpolate
var parsedExp = $parse(‘exp’);for (var i = 0; i < 99999; i++) { parsedExp($scope);}
$eval, $parse, and $interpolate, con’t
● Better to call $parse once and save the function than to call $eval many times
● $parse is much faster than $interpolate, prefer it when possible
for (var i = 0; i < 99999; i++) { $scope.$eval(‘exp’);}
Putting it together
myApp.directive(function($parse) { return { compile: function(elem, attr) { var fooExp = $parse(attr.foo), listExp = $parse(attr.list); return function link(s, elem) { s.foo = fooExp(s); scope.$watchCollection(listExp, function(list) { // do something }); };}};});
myApp.directive(function() { return { link: function(s, elem, attr) { s.foo = scope.$eval(attr.foo); scope.$watch(attr.list, function(list) { // do something }, true); }};});
$watch only what is needed
Sometimes a deep $watch is needed, but not for the entire object.By stripping out irrelevant data, we can make the comparison much faster.
$scope.$watch(‘listOfBigObjects’, myHandler, true);
$scope.$watch(function($scope) { return $scope.listOfBigObjects. map(function(bigObject) { return bigObject.foo. fieldICareAbout; });}, myHandler, true);
$watch before transforming, not after
When applying expensive transformations to input, watch the input itself for changes rather than the output of the transformation.
Example:https://github.com/angular/angular.js/commit/e2068ad426075ac34c06c12e2fac5f594cc81969
ng-repeat - track by $index
By default, ng-repeat creates a dom node for each item and destroys that dom node when the item is removed.
With track by $index, it will reuse dom nodes.
<div ng-repeat=”item in array”> I live and die by {{item}}.<div>
<div ng-repeat=”item in array track by $index”> I live until {{array.length}} is less than {{$index}}.<div>
ng-if vs ng-show
● ng-show hides elements and bindings using css
● ng-if goes a step further and does not even create themo Fewer bindingso Fewer linkers called at startup
● ng-switch is like ng-if in this respect
Not really a Best Practice: $$postDigest
● $$ means private to Angular, so be aware that the interface is not stable
● Fires a callback after the current $digest cycle completes
● Great for updating the dom once after dirty checking is over
Avoiding dirty checking altogether
● Sometimes an expression’s output never changes, but we dirty check it constantly
● Custom directives to the rescue!o Bind once at link and ignore digest cycleso Bind at link and dirty check only on certain eventso https://github.com/kseamon/fast-bind/tree/master/
Implements bind class, href, if, switch, etc left as an exercise to
the reader● Or maybe an optional module in 1.3?
Diagnosing performance problems: Tools
● AngularJS Batarango Great for identifying which $watchers are causing
problems
● Chrome profilero Offers a broader view, but harder to read
● performance.now()o Provides microsecond resolution for measurements
Now use performance.now()
t = 0; c = 0;var myFunc = function() { var d = performance.now(); c++; // some code // some more code t += performance.now() - d;};
Interpreting profiler output: My function
Interpreting profiler output: angular.copy & angular.equals
● If you see these in your profile, you probably have some deep $watches that need to slim down (See Slide 7 and Slide 13)
● Tracking these down is bit tricky - we have to dive into angular.jswindow.slowest = 0;function copy(source, destination, recurse){ if (!recurse) var d = performance.now(), d2; … copy(source, [], true); … if (!recurse && (d2 = performance.now()) - d > slowest) { slowest = d2 - d; console.log(toJson(source), slowest); console.trace(); } return destination;}
An incomplete list of recent improvements to AngularJS
● 1.2.7 emoji-clairvoyance (2014-01-03)o Scope: limit propagation of $broadcast to scopes that have listeners
for the event (80e7a455)
● 1.2.6 taco-salsafication (2013-12-19)o compile: add class 'ng-scope' before cloning and other micro-
optimizations (f3a796e5)o $parse: use a faster path when the number of path parts is low (
f4462319)
● 1.2.5 singularity-expansion (2013-12-13)o $resource: use shallow copy instead of angular.copy (fcd2a813)o jqLite: implement and use the empty method in place of html(‘’) (
3410f65e)
● 1.2.4 wormhole-blaster (2013-12-06)o Scope: short-circuit after dirty-checking last dirty watcher (d070450c)
Future improvements AngularJS
● 1.3o Coalesce asynchronous calls to $apply (#5297)o Built-in bind-once
● Further future (maybe hopefully)o $postDigestWatch - A public watcher based on $$postDigest for dom
updates (Also update ng-bind, ng-class, et al to use it) (#5828)o Watcher deduplication (#5829)o $apply isolation - Set regions (such as dialogs) for which $apply calls
do not affect the whole page (#5830)o Your ideas - File a ticket! Write a pull request!