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
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

<!-- 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

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
  • Lets us use ES6 classes
  • Linter rules:

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

Angular Best Practices and 2.0 Prep

By Christian Yang

Angular Best Practices and 2.0 Prep

  • 620