Optimizing a large angular application (ng conf)

27
Optimizing a Large AngularJS Application Karl Seamon Senior Software Engineer, Google Occasional contributor to AngularJS

Transcript of Optimizing a large angular application (ng conf)

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

Using AngularJS Batarang

Looks like I should check out canInlineEditItem.

Using the profiler

PS: Disable minification or you will be sad

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!

Thanks!