AngularJSのOne-time Bindingを使ってパフォーマンス改善をしよう

AngularJSのOne-time Bindingを使ってパフォーマンス改善をしよう

先生

先生

こんにちは。
先日ng-japanにスポンサー企業として参加してきました、先生です。

本日は、すごくマジメに資料を作ったのに爆笑に包まれてしまったLTで発表した内容を、整理してお送りします。なぜ爆笑されたかについてはここでは語りませんので、直接聞くか動画をご覧ください。

 

当日のLT資料はこちらになります。

One-time Bindingとは

「バインディングを1度しか評価しないようにする機能」です。
通常のバインディングは値が変更されると画面の値も変更されますが、One-time Bindingを使うと描画された以降は再評価されなくなります。

サンプルで実際の動作を見てみましょう。

One-time Bindingのシンプルな例

ソースは至ってシンプルです。

<div id="demo" ng-controller="RootCtrl">
    <div>{{count}}</div><!-- いつものバインディング -->
    <div>{{::count}}</div><!-- One-time Binding -->
</div>
angular.module('app').controller('RootCtrl',[
    '$scope',
    '$interval',
    function($scope,$interval) {
        $scope.count = 0;
        $interval(function() {
            $scope.count++;
        },1000);
    }
]);

1秒毎に$scope.countを増やしていくだけです。
通常のバインディングの方はcountが増える度に画面も変更されますが、One-time Bindingのほうは0のままになっています。

なぜこれが良いのか

単純にパフォーマンスの向上につながります。

その理由は、AngularJSが$scopeの値をチェックして画面に反映している機構にあります。それが$digestで行われているDirty Checkingというものです。

Dirty Checkingとは一定のタイミングで$scopeの値が変更されたかどうかを全てチェックする方式で、AngularJSの場合は$applyを起点としてチェックが行われるようになっています。
泥臭くチェックするからDirty Checkingなんですね。

demo1の例では$timeoutが実行された後に内部で$applyが呼び出されます。

$applyが呼び出されると、値が変更されていないかどうかを$digestでチェックしていきます。ここで$scopeに入っている値が前回から変わっていないかどうかをループしてチェックしていくので、$digest loopなどと呼ばれたりもします。

このチェックフローから、たとえ値を変更していなくても他の$scopeの値に変更があった場合には、値が変わっていないかどうかがチェックされます。
このチェックされる要素の数をWatch数と呼びます。

※ ざっくりしたフローの図
angular-digest-flow

詳しい動作フローについては公式のドキュメントに記載があります。
https://code.angularjs.org/1.3.15/docs/guide/scope#integration-with-the-browser-event-loop

$digestの記述は以下の部分にありますので、どのようにチェックしているか興味がある方はソースを読んでみてください。
https://github.com/angular/angular.js/blob/master/src/ng/rootScope.js#L726-L836

One-time Bindingにすることで、このチェックを1度だけにすることができます。AngularJSのパフォーマンス改善のもっともシンプルな方法はこのWatch数を減らすことにあります。

Watch数の確認方法

そのWatch数がいくつなのかを確認する方法が必要になりますね。

とても簡単にチェックするためにGoogle Chromeの拡張を利用します。

angular-watchers

Angular Watchersの使い方

Chromeウェブストアから上記の拡張機能をインストールします。
AngularJSが使われているサイトをChromeで表示してDevToolsを開きます。

Element, Networkと並んでいるタブに$$watchersという項目が増えているのでそれをクリックします。

angular-watchers-dev

これでDevTools内にWatch数が表示されます。非常に簡単にチェックができるのでオススメです。

さっそく先ほどのサンプルをAngular Watchersで見てみましょう。

One-time Bindingのシンプルな例

すると1 diff: +1と表示されます。このサンプルのWatch数は1。つまり通常のバインディングをしてカウントアップしている部分だけとなります。

angular-watchers-exe

One-time Bindingを使ったWatch数の変化

続いてもう少し実践に近いサンプルでWatch数の変化を見ていきましょう。

以下のようなJSONファイルを用意しました。このような記述が500個続きます。

[
    {
        "id": 1,
        "name": "User1",
        "value": 100
    },
    ... 
]

 

JavaScriptはシンプルに以下のような感じにしました。
USERSには先ほどのJSONデータが入っています。

angular.module('app').controller('RootCtrl',[
    '$scope',
    'USERS',
    function($scope,USERS) {
        $scope.users = USERS;
    }
]);

さっそくこのサンプルをAngular Watchersで見てみましょう。

One-time Bindingを使わない場合

まずは使わない場合のWatch数を見てみます。

<div ng-controller="RootCtrl">
    <ul>
        <li ng-repeat="user in users">
            <div>{{user.id}} - {{user.name}}</div>
            <div>{{user.value}}</div>
        </li>
    </ul>
</div>

One-time bindingを使わない例

Watch数: 1501
usersという配列+({{user.id}}+{{user.name}}+{{user.value}}) が500個で1501になっています。なかなかいい数ですね。一般に2000を超えると重いと感じると言われていますが、この時点でもやや負荷を感じます。

配列に付けた場合

One-time Bindingは配列にも付けることができます。
配列に付けた場合のWatch数を見てみます。

<div ng-controller="RootCtrl">
    <ul>
        <li ng-repeat="user in ::users">
            <div>{{user.id}} - {{user.name}}</div>
            <div>{{user.value}}</div>
        </li>
    </ul>
</div>

::usersとしてusersをOne-time Bindingしました。

配列にOne-time bindingした例

Watch数: 1500
({{user.id}}+{{user.name}}+{{user.value}}) が500個で1500になっています。
配列分だけWatch数が減ったようです。

プロパティに付けた場合

userで参照しているプロパティ全部をOne-time Bindingにしてみます。
配列につけたものは一旦取り除きました。

<div ng-controller="RootCtrl">
    <ul>
        <li ng-repeat="user in users">
            <div>{{::user.id}} - {{::user.name}}</div>
            <div>{{::user.value}}</div>
        </li>
    </ul>
</div>

ループ内のプロパティにOne-time bindingした例

Watch数: 1
users配列だけになったので1になりました。かなり減りました。

全部に付けた場合

usersもプロパティも全部One-time Bindingにしてみました。

<div ng-controller="RootCtrl">
    <ul>
        <li ng-repeat="user in ::users">
            <div>{{::user.id}} - {{::user.name}}</div>
            <div>{{::user.value}}</div>
        </li>
    </ul>
</div>

配列もプロパティもOne-time Bindingした例

Watch数: 0
ついにはWatch数が0になりました。この状態で$scopeの値を変更しても一切チェックされません。

まとめ

AngularJSでもっともシンプルにパフォーマンス改善をする方法は、Watch数を減らすことです。双方向のバインディングが必要のない部分では、適切にOne-time Bindingをしていきましょう。

つい忘れてしまうところだと思いますので、ときどきAngular Watchersを使ってWatch数を計測してパフォーマンス向上を心がけましょう。

 

【AngularJS活用術】

AngularJS勉強会 ng-mtg#6に登壇してきました

LIG主催のAngularJS勉強会 #ngCurryが開催されました

業務で安心して使える厳選AngularJSモジュール8選+α

AngularJSで作成したシングルページアプリケーションをGoogleアナリティクスでトラッキングさせる方法

ECMAScript6で書こう!WebPackとES6-loaderで環境を作り、ES6を先取り体験する方法

LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。

Webサイト制作の実績・料金を見る

この記事のシェア数

CTOの林です。フロントエンドを専門とし、AngularJSのコミュニティをはじめ、様々な勉強会に顔を出しています。効率化マニアでGrunt,Gulpをはじめ、プロジェクト進行やサーバーサイド、インフラ周りの効率化を目指し日々活動しています。 【役職紹介 / CTO】 Web制作にかかる内容を統括しています。プロジェクトを横断的に見渡し、技術的な見地からアドバイスなどを行い、ときには実制作のヘルプを行ったりしています。新人エンジニアの育成や、体外的な活動としてLIG全体のエンジニアのブランディングのため勉強会やイベントへの登壇・開催なども行っています。 【普段やっていること】 新規サービスの設計(UI,遷移,DB,APIなど)、エンジニアの育成及び環境整備、技術選定、Web制作ユニットの統括とプロジェクト進行を円滑にするための仕組みづくり、フロントエンド周りの実装、エンジニアリングの開発効率化、勉強会の登壇・主催など

このメンバーの記事をもっと読む