pirosikick's diary

君のハートにunshift

Speeding up AngularJS apps with simple optimizations

Speeding up AngularJS apps with simple optimizations

ng-newsletterで流れてきて結構いい記事だったのでメモ。

基本的には、$$watchersを減らして、$digestの負荷を下げましょうとのこと。

One Time Binding Syntax {{ ::value }}

1.3.0からの機能。 {{ ::value }}と書くと最初の1度だけバインディングされ、後は$scope.valueが更新されても{{ ::value }}は更新されない。バインディングされた後は監視から外れるので、$digest時の負荷が下がる。

<!DOCTYPE html>
<html ng-app="app">
<body>
<div ng-controller="MainCtrl">

  <h1>{{ title }}</h1><!-- ボタンを押すとどんどん増えていく -->
  <h1>{{ ::title }}</h1><!-- ボタンを押しても更新されない -->

  <button ng-click="plusTitle()">plus</button>
</div>

<script src="bower_components/angular/angular.min.js"></script>
<script>
angular.module('app', [])
  .controller('MainCtrl', function MainCtrl ($scope) { 

    $scope.title = 'Hello!';

    $scope.plusTitle = function () {
      $scope.title = $scope.title + ' ' + $scope.title;
    }

  });
</script>
</body>
</html>

$scope.$apply vs $scope.$digest

$scope.$apply$scope.$digestは、Angularじゃないライブラリのイベント内でDOMを更新したりする場合に、その更新をAngular側にも通知するために使う。 $applyの中で$digestが呼ばれるからどっちも一緒だろと思ってたけど、違うらしい。

$scope.$apply$rootScope.$digestを実行するので全ての$scope$digestが実行されてしまい影響範囲が大きい。 $scope.$digestはその$scopeと子孫の$scope$digestが実行されるので、$scope.$applyに比べると効率がよい。

<!DOCTYPE html>
<body ng-app="app">
<div ng-controller="ParentScopeCtrl">

  <h1>{{ parentObject.value }}</h1>

  <div ng-controller="ChildScopeCtrl1">

    <h2>{{ parentObject.value }}</h2>

    <div ng-controller="ChildScopeCtrl2">

      <h3>{{ parentObject.value }}</h3>

    </div>
  </div>

  <button id="call-apply-button">call $scope.$apply()</button>
  <button id="call-digest-button-1">call $scope.$digest() in ChildScopeCtrl1</button>
  <button id="call-digest-button-2">call $scope.$digest() in ChildScopeCtrl2</button>

</div>

<script src="bower_components/angular/angular.min.js"></script>
<script>

var $scopeOfChildScopeCtrl1, $scopeOfChildScopeCtrl2;

angular.module('app', [])
  .controller('ParentScopeCtrl', function ($scope) { 
    $scope.parentObject = { value: 'parent' };
  })
  
  .controller('ChildScopeCtrl1', function ($scope) {
    // angularの外で$scopeの中身を変更するために、変数に保持
    $scopeOfChildScopeCtrl1 = $scope;
  })

  .controller('ChildScopeCtrl2', function ($scope) {
    // angularの外で$scopeの中身を変更するために、変数に保持
    $scopeOfChildScopeCtrl2 = $scope;
  });

// Angularの監視外でデータを変更し、$apply, $digestの違いを見る、

var callApplyButton = document.getElementById('call-apply-button');
var callDigestButton1 = document.getElementById('call-digest-button-1');
var callDigestButton2 = document.getElementById('call-digest-button-2');

callApplyButton.addEventListener('click', function (e) {

  // $apply => h1, h2, h3、全て更新される
  $scopeOfChildScopeCtrl1.parentObject.value = 'call $scope.$apply().';
  $scopeOfChildScopeCtrl1.$apply();

}, false);

callDigestButton1.addEventListener('click', function (e) {

  // $digest => h2, h3だけ更新される
  $scopeOfChildScopeCtrl1.parentObject.value = 'call $scope.$digest() in ChildScopeCtrl1.';
  $scopeOfChildScopeCtrl1.$digest();

}, false);

callDigestButton2.addEventListener('click', function (e) {

  // $digest => h3だけ更新される
  $scopeOfChildScopeCtrl2.parentObject.value = 'call $scope.$digest() in ChildScopeCtrl2.';
  $scopeOfChildScopeCtrl2.$digest();

}, false);

</script>
</body>

ng-repeatは重い

ng-repeatは使うとすごく気持ちいいし、ulとか繰り返しのDOMを見つけるとついついng-repeatを使ってしまう。 が、やっぱり処理的には重いので、静的にHTML書いて済むところはそうするとよし。

directiveの中ではなるべくDOM操作する

<div ng-show=”something”></div>
$scope.something = false;
$scope.someMethod = function () {
  $scope.something = true;
};

↑みたいにng-show使うために$scopeに変数持つくらいなら、下記のようにDOM操作で解決する。

var menu = $element.find(‘ul’);
menu.hide();
$scope.someMethod = function () {
  menu.show();
};

自作Directiveの場合は、コンストラクタの引数に$elementが渡されるのでそれを使えよとのこと。 たかがng-showでもng-repeatと併用で使われた時に爆発的に監視対象が増える可能性があるので要注意。

まとめ

  • 双方向データバインディングフレームワークは便利だけど内部の動作をある程度知ってないと気付かぬうちにひどいパフォーマンスになりそう。 このあたりの動作の詳しいところはオライリーの本に書いてあるのだろうか。
  • 確かEmber.jsは、「この関数はこの値とこの値を更新しますー」みたいなのが定義できて、監視を効率的にできる機能があった気がする。

早くオライリーの本読まないとー