Angular Best Practices and
2.0 Prep
Overall References
Performance Tips
Cheat Sheet
- Optimize digest cycle: all about $$watchers
- $scope.$apply() vs $scope.$digest()
- One-time bindings
- $watchCollection() vs $watch()
- Debounce model changes
- ng-if vs ng-show (or ng-hide)
- Use `track by` with ng-repeats
- Profile using Angular Batarang
The $digest() cycle
- Triggered by:
- User action (ng-click etc)
- ng-change
- ng-model
- $http events
- $q promises resolved
- $timeout
- $interval
- Ultimately: call to $scope.apply or $scope.digest
- By default evaluates ENTIRE app's watch list MULTIPLE times until results come back same (up to 10 times)
http://www.alexkras.com/11-tips-to-improve-angularjs-performance/#watchers
$scope.$apply() vs $scope.$digest()
- $digest() acts on specified scope and child scopes
- $apply() acts on $rootScope and therefore ALL scopes
- Pseudo-code from Angular docs:
function $apply(expr) {
try {
return $eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
$root.$digest();
}
}
If appropriate, using $scope.$digest() can lead to big savings
Example for $timeout()
setTimeout(() => {
// Your code
$scope.$digest()
}, 0);
$timeout(() => {
// Your code
}, 0); // Optional, defaults to 0
Default: calls $apply() implicitly
Potential change:
One-time bindings {{::prop}}
// MyDirective.js: a (possibly 3rd party) directive that has a bidirectional binding
app.directive('MyDirective', function() {
return {
scope: {
name: '='
}
};
});
// app.html: if our app will never change the value, pass a one-time binding
<div my-directive name="::myName"></div>
https://docs.angularjs.org/guide/expression#one-time-binding
$watchCollection() vs $watch(exp, listener, true)
- Third param to $watch() is objectEquality
- Does a deep comparison (i.e. nested props)
- Also saves deep copy to do the comparison
$scope.$watch('currentUser', (updated) => {
$log.log(`New user name is ${updated.name}`);
}, true);
$watchCollection()
Best to still just watch single prop:
$watch('obj.prop', listener);
$scope.$watchCollection('currentUser', (updated) => {
$log.log(`New user name is ${updated.name}`);
});
$watchCollection() compares:
- For objects: first level props only
- Arrays: Reference equality of elements
$scope.$watch('currentUser._rId', (updated) => {
$log.log(`New user name is ${updated.name}`);
});
Debounce model changes
<input
type="text"
placeholder="Search"
ng-model="q"
ng-model-options="{debounce: {'default': 500, 'blur': 0 } }"
/>
<input
type="text"
ng-model="searchOrg"
typeahead="org as org.name for org in getOrgs($viewValue)"
typeahead-wait-ms="250"
/>
For typeaheads:
ng-if vs ng-show (or ng-hide)
ng-if
- Removes and recreates DOM element
- Can be expensive for complicated directives
- Removes all watchers
ng-show / ng-hide
- Element always present, hidden with CSS display: none
- Watchers remain + are evaluated in $digest loop
Use `track by` with ng-repeats
- By default, Angular adds a $$hashKey prop to each model to determine whether to recreate DOM
- All DOM lost and recreated if whole list replaced:
http://www.codelord.net/2014/04/15/improving-ng-repeat-performance-with-track-by/
<ul class="tasks">
<li ng-repeat="task in tasks">
{{task.id}}: {{task.title}}
</li>
</ul>
$scope.tasks = newTasksFromTheServer;
Solution: use `track by` and specify a prop:
<ul class="tasks">
<li ng-repeat="task in tasks track by task.id">
{{task.id}}: {{task.title}}
</li>
</ul>
Profiling with Angular Batarang
Setup
- Link to Chrome app
- Disable string dependency injection:
<!-- public/index.html (can just adjust the built file) -->
<!-- Change this line: -->
<html class="no-js" ng-app="clPlatform.app" ng-strict-di>
<!-- To this (remove `ng-strict-di` attribute): -->
<html class="no-js" ng-app="clPlatform.app">
Activate in Developer Tools
Profile all watch expressions
Best Practices + 2.0 Prep
Get rid of $scope, use `controllerAs`
Currently:
// TodoCtrl.js
app.controller('TodoCtrl', function ($scope) {
$scope.input = 'ex. buy milk';
$scope.$watch('input', (newVal) => {
// Do something
};
});
// TodoCtrl.js
app.controller('TodoCtrl', function ($scope) {
const vm = this;
vm.input = 'ex. buy milk';
$scope.$watch('vm.name', (newVal) => {
// Do something
});
});
// todo.html
<div>
<input type="text" ng-model="input" />
</div>
// app.js
app.config(($stateProvider) => {
$stateProvider
.state('todo', {
...
controller: 'TodoCtrl',
controllerAs: 'vm',
...
});
});
// todo.html
<div>
<input type="text" ng-model="vm.input" />
</div>
Instead:
Why?
Faster | Angular 2.0 | Style/Linter | Because Reasons |
---|---|---|---|
- $scope is going away in Angular 2.0
- From Angular ngController docs:
- Explicitness when multiple controllers
- ES6 class friendly (more to come)
- No prototypal inheritance masking of primitives
- Linter rules:
Directives:`controllerAs`, `bindToController`
app.directive('TodoDirective', function () {
return {
scope: {}, // Doesn't have to be isolate scope
bindToController: {
input: '='
}
controller: function () {
this.input = 'ex. buy milk';
},
controllerAs: 'vm',
...
};
});
Use ES6 classes and DI through static $inject for everything
Currently:
// MyController.js
const ctrlFn = ($log) => {
this.sayHello = function () {
$log.log('hello');
};
};
ctrlFn.$inject = ['$log'];
angular.module('MyApp').controller('MyController', ctrlFn);
// MyController.js
export default class MyController {
static $inject = ['$log'];
constructor($log) {
this.$log = $log;
}
sayHello() {
this.$log.log('hello');
}
}
// Following would go away in Angular 2.0
angular.module('MyApp').controller('MyController ', MyController);
Instead:
Why?
Faster | Angular 2.0 | Style/Linter | Because Reasons |
---|---|---|---|
Tentative: Use .service() instead of .factory()
Currently:
// Service: exports a constructor that gets new'd
angular.module('MyApp')
.service('MyService', function () {
this.sayHello = function () {
console.log('hello');
};
});
// Factory: exports a function
angular.module('MyApp')
.factory('MyService', function () {
return {
sayHello: function () {
console.log('hello');
};
}
});
Instead:
// MyService.js
export default class MyService {
// Alternative dependency injection style, to-be-discussed
// static $inject = ['$log'];
constructor($log) {
this.$log = $log;
}
sayHello() {
this.$log.log('hello');
}
}
// Following just goes away when we upgrade
angular.module('MyApp').service('MyService', ['$log', MyService]);
Why?
Faster | Angular 2.0 | Style/Linter | Because Reasons |
---|---|---|---|
- Lets us use ES6 classes for services
- Debate: In opposition to linter rule no-service-method
Cleanup Dependencies
- No unused deps (di-unused)
- No browser globals, inject Angular version
- All Angular Wrapper linter rules
Angular Best Practices and 2.0 Prep
By Christian Yang
Angular Best Practices and 2.0 Prep
- 620