AngularJSのOnetime bindingを使わなくても大してパフォーマンスは悪くならない話


# 結論

先に結論を書きますと、AngularJS 1.3.0-rc.2以降はバインディングの最適化が行われているので、Onetime bindingを使っても劇的にパフォーマンスがよくなる。ということはありませんでした。

# 背景

（特に聞きたくない人は読み飛ばしてください）

AngularJSといえばパフォーマンスに関して話題になることが多いですが、その中で良く耳にするのが下記二つです

- __バインディングの数は2,000以下にしよう__
- __Onetime bindingを使ってバインディング数を削減しよう__

僕もこれにならい、可能な限り全てのバインディングをOnetime bindingに変更してきましたし、他のメンバーのコードレビュー時でもバインディング数が増えないように目を光らせてきました。

# Onetime bindingの問題点

しかし、Onetime bindingを多用していると、何らかの形で表示中のデータが更新されて、その更新結果を画面に反映させたい場合に、不都合が起こることがあります。たとえば下記のようなユーザー一覧で、ユーザーが持つ一部のデータ（info）を更新するAPIを呼び出して、返ってきたデータを画面に反映したいときです。

```html
<ul ng-controller="myCtrl">
  <li ng-repeat="user in users">
    <div>{{ ::user.name }}</div>
    <div>{{ ::user.info.email }}</div>
    <div>{{ ::user.info.age }}</div>
    <button ng-click="updateInfoClicked(user)">Update info</button>
  </li>
</ul>
```

```js
app.controller('myCtrl', function($scope, userInfoApi) {
  $scope.users = [
    { 'id': 1, 'name: 'Taro',   'info': { 'id': 1, 'email': 'taro@example.com', 'age': 20 }},
    { 'id': 2, 'name: 'Jiro',   'info': { 'id': 2, 'email': 'jiro@example.com', 'age': 18 }},
    { 'id': 3, 'name: 'Saburo', 'info': { 'id': 3, 'email': 'saburo@example.com', 'age': 14 }}
  ];
  $scope.updateInfoClicked = function($event, user) {
    userInfoApi.save({ 'id': user.info.id, user.info, function(updatedInfo) {
    	user.info = updatedInfo;
    });
  };
});
```

上記のように、user内のinfo情報のみを更新して、それを画面に反映させたい場合、上記コードのままだとOnetime bindingの影響で画面に反映されません。そのためにng-repeatにtrack byをつけたり`users = angular.copy(users)`とかしてみたり[kcd-recompile](https://gist.github.com/kentcdodds/739566ebd3609ac95e61)を使ってng-repeat内を再コンパイルしてみたり、最終的には通常のバインディングにしたりしてなんとか画面に反映させようとするわけですが、

- 「開発効率を上げたくてbinding（AngularJS）を使う」⇒
- 「パフォーマンスに影響あるからOnetime bingindを使う」⇒
- 「その影響で画面に反映されない箇所が出てくるからなんとか反映させる」

みたいな感じで、何をやっているんだろう・・・という気分になってきます。


で、本当にOnetime bindingは効果があるのか、パフォーマンスへの影響が分かるサンプルコードを作成していたところ__「実は大してパフォーマンスに影響ないんじゃないか！？」__という結論に至りました。

# なぜ影響ないのか

[AngularJSのChange Log](https://github.com/angular/angular.js/blob/master/CHANGELOG.md)
で、1.3.0-rc.2を見ると、パフォーマンス改善（Performance Improvement）の箇所に`execute watched expressions only when the inputs change (fca6be71, #9006, #9082)`という変更があります。これは、たとえば`<div>{{ a * b }}</div>`のようなバインディングをdigest loopごとに素直にそのまま計算するのではなく、`a`と`b`のみを$watchして、digest loopの検証コストを少なくする、という変更です。これによって今まではinput要素でテキスト1文字入力する度にdigest loopが走ってバインディング数が10,000だったら10,000回の検証コード（計算やfilterやその他）が走るところを、10,000回の`hoge == oldHoge`などで済むようになってます。

# サンプル

それを体感できるサンプルを作りました。内容はすごくシンプルな、大量の行列がある`<table>`で、大量のバインディングを使っています

- [AngularJS 1.3.0-rc.1を使ったサンプル（重いので注意）](https://embed.plnkr.co/1ltyisUjsHZ0kCgfDWKK)
- [AngularJS 1.3.0-rc.2を使ったサンプル](https://embed.plnkr.co/C9DSBttQjNmOHBlz4xVH)

両方ともバインディング数は31,003です。
1.3.0.rc-1のほうだと、`<input>`にテキストを入力する度に大量のfilter処理が呼ばれ、かなり重いのが分かります。（下記イメージ）

![capture.gif](https://qiita-image-store.s3.amazonaws.com/0/7980/5cad4406-3890-0c4a-a1c8-9f04c8a0ba7c.gif)

しかし、1.3.0-rc.2のほうだと、`<input>`にさくさく値を入力でき、入力の度にfilterも呼ばれません（下記イメージ）

![capture.gif](https://qiita-image-store.s3.amazonaws.com/0/7980/589dc611-495d-0e6d-539a-41532354de21.gif)

バインディング数は同じですが、かなりの改善が見られます。重たいfilter処理をしていればなおさら、この効果は顕著に現れます。もうひとつ、1.3.0-rc.2でOnetime bindingを使ったサンプルです。

- [AngularJS 1.3.0-rc.2 Onetime bindingを使ったサンプル](https://embed.plnkr.co/P1JdGZdET1oCq8qUdj2d/)

![capture3.gif](https://qiita-image-store.s3.amazonaws.com/0/7980/fbc1a520-095f-3ed8-4922-0aafc00757a0.gif)

バインディング数は5と、かなり差がありますが、体感レベルで問題になるほど違いはありませんでした。（正確に計測すればもちろん違いはあると思いますが、あくまで体感レベルです）

# 注意点

この改善はAngular式をparseした時に行われるため、下記のように独自の検証関数を渡した場合は今までと変わらず呼ばれてしまうので注意が必要です。

```js
scope.$watch(function() {
  return isChanged(scope.hoge);
}, function(newValue) {
  // hoge
});
```

1.3.0-rc.2で上記のような独自watch式を入れたものを作りました。少し重いです。

- [AngularJS 1.3.0-rc.2で独自$watchを使ったサンプル](https://embed.plnkr.co/C9DSBttQjNmOHBlz4xVH)

![capture.gif](https://qiita-image-store.s3.amazonaws.com/0/7980/449c8b3d-199a-b790-050d-ec97e3b27cc2.gif)

# まとめ

- Onetime bindingを使わなくてもAngularJS 1.3.0からバインディングのパフォーマンスが大きく改善されている（バインディング数が少ないに越したことはないですが）
- `$watch(function() { })`で独自関数でチェックは極力やめる

# 最後に

ということで、今後はバインディング数だけではなく、$watchの内容やバインディングが最適化される書き方になっているかどうかを観点にコードレビューしようと思います