angular performance: then, now and the future. todd motto
TRANSCRIPT
Topics
» New features (1.2 - 1.3+)
» Generic changes
» Perf features
» Performance driven Angular
» $digest loop/$$watchers/$$asyncQueue
» Quick wins, tips and tricks
» Structure practices, advanced techniques
» IE8 support dropped
» DOM manipulation
» ~4.3 times faster
» 73% less garbage
» $digest loop
» ~3.5 times faster
» 78% less garbage
» 400+ bug fixes
» One-time bind syntax
» ngModelOptions
» bindToController property
» ngModel.$validators
» ngMessage/ngMessages
» strictDI
» $applyAsync in $http
» Disable debug info
one time bindings<p>{{ ::vm.name }}</p>
<p ng-bind="::vm.name"></p>
<div ng-if="::vm.user.loggedIn"></div>
<div ng-class="::{ loggedIn: vm.user.loggedIn }"></div>
<ul> <li ng-repeat="user in ::vm.users"> {{ ::user.name }} </li></ul>
one time bindings» Defined with ::
» $watched until not "undefined"
» $$watcher is unbound
» Will not update upon Model changes
» One-time, not one-way
» Great for single static rendering
ng-Model-Options<!-- updateOn --><input type="text" ng-model="vm.model" ng-model-options="{ updateOn: 'default blur' }">
ng-Model-Options<!--debounce:- example will debounce 250ms when typing- example will update model immediately on "blur"--><input type="text" ng-model="vm.model" ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 250, 'blur': 0 } }">
ng-Model-Options// directive controllerfunction FooDirCtrl() { // undo model changes if (condition) { this.model.$rollbackViewValue(); }}
ng-Model-Options» Fine tune how Model updates are done
» Define event types
» Add debounce to delay Model synchronisation
» e.g. { debounce: 250 } = $digest ~250ms
» $rollbackViewValue for undoing model changes
bindToController// directive controllerfunction FooDirCtrl() { this.bar = {}; this.doSomething = function doSomething(arg) { this.bar.foobar = arg; }.bind(this);}
bindToController// directive controllerfunction FooDirCtrl($scope) { this.bar = {}; this.doSomething = function doSomething(arg) { this.bar.foobar = arg; // reference the isolate property $scope.name = arg.prop; }.bind(this);}
bindToControllerfunction fooDirective() { return { ... scope: {}, bindToController: { name: '=' }, ... };}
bindToController// directive controllerfunction FooDirCtrl() { this.bar = {}; this.doSomething = function doSomething(arg) { this.bar.foobar = arg; // reference the isolate property this.name = arg.prop; }.bind(this);}
bindToController» Used with "controllerAs" (class-like)
» Binds isolate props to the Controller instance
» No $scope
» $scope remains "special use only"
» Not used for data
» Used for $watch/$on/etc
ngModel.$validators// old schoolfunction visaValidator() { var VISA_REGEXP = /^4[0-9]{12}(?:[0-9]{3})?$/; function link($scope, element, attrs, ngModel) { ngModel.$parsers.unshift(function (value) { var valid = VISA_REGEXP.test(value); ngModel.$setValidity('visaValidator', valid); return valid ? value : undefined; }); } return { require : 'ngModel', link : link };}
angular.module('app').directive('visaValidator', visaValidator);
ngModel.$validators// new schoolfunction visaValidator() { var VISA_REGEXP = /^4[0-9]{12}(?:[0-9]{3})?$/; function link($scope, element, attrs, ngModel) { ngModel.$validators.visaValidator = function (value) { return VISA_REGEXP.test(value); // Boolean }; } return { require : 'ngModel', link : link };}
angular.module('app').directive('visaValidator', visaValidator);
ngModel.$validators<form name="myForm"> <label> <input type="text" ng-model="myForm.card" visa-validator> <div ng-if="myForm.myPassword.$error.visaValidator"> Not a valid VISA format! </div> </label></form>
ngModel.$validators» ngModel.$validators Object
» Instead of $parsers/$formatters
» Return a Boolean from the bound function
» Use with the $error Object in the View
ngMessage/ngMessages<form name="myForm"> <label> Enter email: <input type="text" ng-model="field" name="myField" required ng-minlength="5" ng-maxlength="100"> </label> <div ng-messages="myForm.myField.$error" role="alert"> <div ng-message="required"> You did not enter a field </div> <div ng-message="minlength, maxlength"> Your email must be between 5 and 100 characters long </div> </div></form>
strictDI// implicit annotationfunction SomeService($scope, $timeout) { //...}
angular .module('app') .factory('SomeService', SomeService);
strictDIfunction SomeService($scope, $timeout) { //...}
// Array annotationsSomeService.$inject = ['$scope', '$timeout'];
angular .module('app') .factory('SomeService', SomeService);
strictDI» Runs the application's $injector in strict mode
» Throws an error on Services using implicit annotations
» Use ng-annotate to automate this process
$applyAsync with $httpfunction config($httpProvider) { $httpProvider.useApplyAsync(true);}angular .module('app', []) .config(config);
More:blog.thoughtram.io/angularjs/2015/01/14/exploring-angular-1.3-speed-up-with-applyAsync.html
$applyAsync with $http» Enables $applyAsync to be used with $http
» Schedules an async $apply for batched requests
» For requests that resolve within ~10ms
» Pushes into $$asyncQueue
» Single $digest
Disable debug infofunction config($compileProvider) { $compileProvider.debugInfoEnabled(false);}angular .module('app', []) .config(config);
Disable debug info<!-- enabled --><div ng-controller="MainCtrl as vm" class="ng-scope ng-binding"> <my-directive class="ng-isolate-scope"> // content </my-directive></div>
<!-- disabled --><div ng-controller="MainCtrl as vm"> <my-directive> // content </my-directive></div>
Disable debug info» Disable in production for performance boosts
» Removes $scope references on elements
» Doesn't add classes to DOM nodes with binding info
» Enable in console with angular.reloadWithDebugInfo();
$digest: $digest loop» Triggered by $scope.$apply / built-in events
» Iterates $$watchers Array on $scope
» If model value is different from last calculated then corresponding listener executes
» Exits loop, Angular loops again (10 max)
» Repaints DOM (View expressions updated)
$digest: $$watchers» View events/bindings {{ foo }}
» Angular adds a watch to the $watch list
» Only $watched if bound in the View
» Dirty checked in the $digest loop
» Minimise use of $$watchers / avoid if possible
$digest: $$asyncQueue» $evalAsync
» Runs first in $digest
» May run $digest again to flush $$asyncQueue
track by<!-- after --><ul> <li ng-repeat="user in vm.users track by user.id"> {{ user.name }} </li></ul>
track by» Minimal DOM repaints (only what's changed)
» Uses Object references instead of Angular hashes
ng-if / switch vs ng-show / hide<!-- ng-show --><ul ng-show="vm.exposeNav"> <li ng-repeat="menu in vm.menus"></li></ul>
<!-- ng-if --><ul ng-if="vm.exposeNav"> <li ng-repeat="menu in vm.menus"></li></ul>
ng-if / switch vs ng-show / hide» ng-if/switch reconstruct the DOM
» ng-if/switch for less frequent/heavier rendering
» ng-show/hide toggle "ng-hide" class
» ng-show/hide for more frequent/lighter rendering
» ng-show/hide less performant due to $$watchers (when hidden)
ng-bind over {{ handlebars }}<!-- handlebars --><p>{{ vm.username }}</p>
<!-- ng-bind --><p ng-bind="vm.username"></p>
<!-- perf example --><p> Welcome <span ng-bind="vm.username"></span> to Facebook</p>
ng-bind over {{ handlebars }}» No DOM flicker (invisible bindings) with ng-bind
» Significantly faster
» ng-perf.com/2014/10/30/tip-4-ng-bind-is-faster-than-expression-bind-and-one-time-bind
» Lesser need for ng-cloak
» Angular won't evaluate entire text content
$apply or $digest?// forces a $rootScope.$digest();$scope.$apply();
// forces a [current $scope].$digest();$scope.$digest();
$apply or $digest?» $scope certainties
» Prevent a full $rootScope.$digest() if you're certain only child $scopes need updating
» Improve performance by not forcing a full $rootScope.$digest
» $scope.$digest runs on current and child $scopes
» $scope.$apply triggers $rootScope.$digest call
$destroy unbindingfunction myFunction () { // handle element clicks}
// bindelement.on('click', myFunction);// unbind$scope.$on('$destroy', function () { element.off('click', myFunction);});
$destroy unbinding» Remove event listeners that may cause memory leaks
» DOM nodes that are destroyed
» Manually unbind by listening to $destroy
» $scope.$on events are automatically removed
Deep $watch vs $watchCollectionvar prop = [{...},{...},{...},{...}];
$scope.$watch('prop', function (newValue, oldValue) {
}, true);
$scope.$watchCollection('prop', function (newValue, oldValue) {
});
Deep $watch vs $watchCollection» Deep $watch uses deep Object tree comparison
(expensive)
» $watchCollection goes only one level deep
» Shallow reference of all top level items
» Try not to use either unless you have to
» Not as testable
» Signs of bad architecture
» Litter Controllers
avoiding DOM filtersfunction SomeCtrl($filter) { // date passed in elsewhere var time = 1444175093303; // Parsed in JS before bound to the DOM this.parsedDate = $filter('date')(time, 'dd-MM-yyyy');}
angular .module('app') .controller('SomeCtrl', SomeCtrl);
avoiding DOM filters» DOM filters run twice per $digest
» Preprocess in a Controller
» Bind parsed value to the View
Takeaways» Understand the $digest loop
» Investigate the performance side of each Directive/API you use
» Consider using JavaScript over DOM bindings where possible ($filter etc.)
» Check Angular's GitHub repo for changelogs/releases
Thank [email protected]/toddmotto