Dr. GlebのAngularアプリ最適化TIPSまとめ

Dr. Glebのブログ記事 Improving Angular web app performance example. | Better world by better software を簡単にまとめておこうと思う。


try-catchはできるだけ使わない

V8ではtry-catchを使った処理は最適化されないので、特にループ処理ではできるだけ使わないようにする。Primesの例ではtry-catchを無くしただけで全体の処理は2倍以上、メソッド単体では200倍近く高速化している。

追記15.08.26:$apply()や$digest()でtry-catchが使われていることが判明(ノ∀`)
琴線探査: これがAngularJSの$apply()や$digest()が低速な理由のひとつか?


ダイジェストサイクルをできるだけ速くする

$scope.$apply()した時のあれ。ダイジェストサイクルはフレームレートと同じで、長くなるほどUIが固まってしまうことになる。短くする方法は色々ある。


バインドさせる変数をできるだけ少なくする

ng-modelとか$scope.valueとか。バインドさせる変数の数だけダイジェストサイクルは遅くなる。


filterはできるだけ使わない

filterはダイジェストサイクルを遅くさせる。「{{ "index" | lowercase }}」的な無駄なフィルターはもってのほか。


one-time-bindingを使う

サーバーから持ってくるデータは大抵の場合は大量だ。これをページにレンダリングするために2wayバインディングを使ってしまうとダイジェストサイクルが大幅に遅くなる。

そこでAngular1.3から導入されたone-time-bindingを使う。これを使えば、始めだけはバインディングするのでダイジェストサイクルに影響するが、その後は影響が無くなる。

例えば「{{name}}」としている所を「{{::name}}」とするだけでone-time-bindingになる。

AngularJS: Developer Guide: Expressions


React的なレンダリング方法に変更する

特にtableの中身などの反復部分で。要するに、こういった部分を

<tr ng-repeat="prime in primes | orderBy:$index " bindonce>
  <td>index</td>
  <td bo-text="$index + 1 | number:0" />
  <td>prime number</td>
  <td bo-text="prime | number:0" />
  <td>is prime? true</td>
</tr>

こうする。

// use AngularJs built-in filter
var number = $filter('number');
function generateTableRows() {
  var k;
  var str = '';
  for(k = 0; k < $scope.n; k += 1) {
    str += '<tr><td>index</td>';
    str += '<td>' + number(k + 1, 0) + '</td>';
    str += '<td>prime number</td>';
    str += '<td>' + number($scope.primes[k], 0) + '</td>';
    str += '<td>is prime? true</td></tr>';
  }
  document.getElementsByTagName('table')[0].innerHTML = str;
}
$scope.find = function () {
  // generate primes list as before
  generateTableRows();
}

これだけで10倍速くなる。

※後に出てくるバーチャルスクローリングの方が現実的と思われ


表示できるものはできるだけ早く表示する

素数の計算は分単位の時間がかかる場合があるが、始めの100個は非常に速く計算できる。まずこれを表示し、残りは後で表示するようにする。

例えば、setTimeout()や$timeoutを使って長い処理を分割する。

実際に処理が速くなるわけではないが、心理的な処理速度は大幅に速くなる。UX的な配慮と言えるだろう。


UIの更新サイクルを33msec以内に保つ

UIの更新サイクルには

・JSコード実行
・レイアウト計算
・各コンポーネントをバックバッファーにレンダリング
・各コンポーネントを全部合わせてペイント

の4つのステップがあるが、これを最低33msec以内に保つようにする。これで30fps。60fpsが必要なら16msec以内に保つ必要がある。

ブラウザの性能やハードウェアの性能に依存する部分も多いが、コーダーとして最もコントロールできる部分はJSコードの実行だろう。ここをできるだけ速くすることが大事。

33msec以上かかるような処理は$qや$timeoutを駆使して処理を分割し、計算>表示>計算>表示>…と少しずつ計算しては表示するというサイクルを作る。


テーブルに行を追加する時はテーブル自体を追加する

大量にデータを含むテーブルにこのような処理をすると、全ての行でレイアウトの再計算が行われてしまい非常に遅くなる。

function generateTableRows(first, last) {
  // generate new rows HTML markup into variable str
  document.getElementsByTagName('tbody')[0].innerHTML += str;
}

そこで、行を追加せずテーブル自体を追加する。

function generateTableRows(first, last) {
  var k, txt = angular.bind(document, document.createTextNode);
  var table = document.createElement('table');
  for(k = first; k < last; k += 1) {
    var row = table.insertRow();
    row.insertCell().appendChild(txt('index'));
    row.insertCell().appendChild(txt(k + 1));
    row.insertCell().appendChild(txt('prime number'));
    row.insertCell().appendChild(txt($scope.primes[k]));
    row.insertCell().appendChild(txt('is prime? true'));
  }
  // schedule DOM update by attaching new table element to the body
  document.body.appendChild(table);
}

※後に出てくるバーチャルスクローリングの方が現実的と思われ


時間のかかる処理をWebWorkerに投げてしまう

結構面倒だが、奥の手としては十分検討の価値がある。


Array.pushはできるだけ使わない

Array.pushを使うとメモリを効率的に使えないため頻繁にGCが発生し、パフォーマンスに影響する。

var k, n = numbers.length;
for(k = 0; k < n; k += 1) {
  $scope.primes.push(numbers[k]);
}

配列の大きさが決まっている場合は、先にその大きさで配列を確保して値をコピーするようにする。

// initialize the array length
$scope.primes = new Array($scope.n);
$scope.computedN = 0;
// copy numbers
var k, n = numbers.length;
for(k = 0; k < n; k += 1) {
  $scope.primes[$scope.computedN] = numbers[k];
  $scope.computedN += 1;
}


必要になった時に必要なだけ計算(表示)する

全てのデータが必要とは限らないので、「ngInfiniteScroll」などを使ってリストの最後まで行ったら素早く計算して表示するようにする。


$scope.$watch()をやたらと使わない

// instead of individual actions for same watcher
$scope.$watch(function () {
  return $scope.primes;
}, foo, true);
$scope.$watch(function () {
  return $scope.primes;
}, bar, true);
$scope.$watch(function () {
  return $scope.primes;
}, baz, true);
// use single watcher and fire off multiple actions
$scope.$watch(function () {
  return $scope.primes;
}, function () {
  foo();
  bar();
  baz();
}, true);


バーチャルスクローリングを使う

ngRepeatでバーチャルスクロール機能を実現するライブラリ 「kamilkp/angular-vs-repeat」。万単位のリストであっても、実際に見えるデータのDOMのみを保持するようにすることでメモリ面でも速度面でもパフォーマンスの向上が期待できる。Dr.Glebの実験でもかなりのパフォーマンスを叩き出しているようだ。


Angularのバージョンを上げる

1.2で850msかかっていた処理が1.3にしただけで250msに!


ng-classをやたらと使わない

ng-class内のexpressionを処理するのに時間がかかるのでやたらと使わない。例えばこのような処理は

<td>
  <input type="number"
         ng-model="prime" name="primeNumber" required=""
         ng-class="{ 'small': prime < 10, 'exact': prime == 10, 'large': prime > 10 }"
         />
</td>

このようにmaxを設定して

<td>
  <input type="number" max="10"
         ng-model="prime" name="primeNumber" required=""
         />
</td>

フォームのバリデーションでng-validやng-invalidのCSSクラスが付加されるようにして、そこで必要なスタイルをつける。


まとめ

なんだかAngular独特の機能をできるだけ使わない方がいいという感じを受けた(^^);

Angularが便利だからとあまり依存しすぎないようにして、必要最小限の利用にとどめた方が、コードのメンテナンス性も含めた全体のパフォーマンスが上がりそうだなと思った。

これらのTIPSの中では特にone-time-bindingとバーチャルスクローリングの効果が高そう。


こちらも良記事。

Ultimate AngularJS and Ionic performance cheat sheet | Julien Renaux Blog

やっぱりone-time-bindingは共通して言及されている。

コメント

このブログの人気の投稿

レオナルド・ダ・ビンチはなぜノートを「鏡文字」で書いたのか?

Google DriveにCURLでアップロードするには?

macでsmb(samba)共有サーバーに別名で接続(別アカウント名で接続)する方法