ジャンク バストによるレンダリング パフォーマンスの向上

Tom Wiltzius
Tom Wiltzius

はじめに

アニメーション、遷移、その他の小さな UI 効果を実行するときに、ウェブアプリが応答性とスムーズさを感じられるようにする必要があります。こうしたエフェクトにジャンクをなくすことで、「ネイティブ」な印象と、ぎこちなく粗雑なエフェクトとの違いが生まれます。

これは、ブラウザでのレンダリング パフォーマンスの最適化について説明する一連の記事の最初のパートです。最初に、スムーズなアニメーションが難しい理由と、それを実現するために必要なこと、簡単なベスト プラクティスをいくつか紹介します。これらのアイデアの多くは、今年の Google I/O のトーク(動画)で Nat Duca と私が行った講演「Jank Busters」で発表されたものです。

V-sync の概要

PC のゲーマーには「v-sync」という用語がありますが、ウェブではあまり見覚えがありません。

スマートフォンのディスプレイについて考えてみましょう。画面は定期的に更新されます(通常は 1 秒に 60 回程度です)。V 同期(垂直同期)とは、画面を更新するたびに新しいフレームを生成することを指します。これは、画面バッファにデータを書き込むプロセスと、そのデータを読み取ってディスプレイに表示するオペレーティング システムとの間の競合状態と考えることができます。バッファリングされたフレーム コンテンツは、更新中ではなく、これらの更新の間に変更されるようにする必要があります。そうしないと、モニターにフレームの半分ともう半分が表示されて、「テアリング」が発生します。

スムーズなアニメーションを作成するには、画面が更新されるたびに新しいフレームを準備する必要があります。これには 2 つの大きな影響があります。フレーム タイミング(フレームの準備が必要なタイミング)とフレーム バジェット(ブラウザがフレームを生成する必要がある時間)です。1 つのフレームを完了するまでの時間が画面更新の間隔(60 Hz の画面では約 16 ミリ秒)しかなく、最後のフレームが画面に現れたらすぐに次のフレームの生成を開始したいと考えています。

タイミングがすべて: requestAnimationFrame

多くのウェブ デベロッパーは、16 ミリ秒ごとに setInterval または setTimeout を使用してアニメーションを作成しています。これはさまざまな理由から始まりますが(これについては後で詳しく説明します)、とりわけ次の懸念事項があります。

  • JavaScript のタイマーの解像度は数ミリ秒程度
  • リフレッシュ レートはデバイスによって異なる

前述のフレーム時間の問題を思い出してください。次の画面更新が発生する前に、JavaScript、DOM 操作、レイアウト、ペイントなどで完成したアニメーション フレームの準備が整っている必要があります。タイマーの解像度が低いと、次の画面更新までにアニメーション フレームを完了するのが難しくなる可能性があります。ただし、画面のリフレッシュ レートが変動すると、固定タイマーでは不可能になります。タイマー間隔が何であれ、フレームのタイミング ウィンドウから徐々にずれて、最終的にフレームが 1 つ破棄されます。この現象は、タイマーがミリ秒単位の精度で発動した場合でも発生します(デベロッパーが検出しているように)。タイマーの解像度は、マシンのバッテリー接続状態か電源接続状態かによって異なり、バックグラウンドのタブがリソースを占有しているなどの影響を受けることがあります。まれなケースでも(たとえば、16 フレームごとに 1 ミリ秒間ずれたため)、1 秒ごとにフレームがドロップします。また、表示されないフレームを生成する作業も行われるため、アプリの他の処理に費やす可能性のある電力と CPU 時間が無駄になります。

ディスプレイによってリフレッシュ レートが異なります。60Hz が一般的ですが、一部のスマートフォンは 59Hz ですが、低電力モードで 50Hz に低下するノートパソコン、70Hz のデスクトップ モニターもあります。

レンダリング パフォーマンスの議論では 1 秒あたりのフレーム数(FPS)に着目する傾向がありますが、ばらつきはさらに大きな問題になる可能性があります。アニメーションのタイミングが合わないと、アニメーションに小さく不規則な問題が生じることがあります。

アニメーション フレームのタイミングを正しく設定するには、requestAnimationFrame を使用します。この API を使用すると、ブラウザにアニメーション フレームを要求します。コールバックは、ブラウザがまもなく新しいフレームを生成するときに呼び出されます。これは、リフレッシュ レートに関係なく行われます。

requestAnimationFrame には他にも便利なプロパティがあります。

  • バックグラウンド タブのアニメーションは一時停止するため、システム リソースとバッテリーを節約できます。
  • システムが画面のリフレッシュ レートでレンダリングを処理できない場合、アニメーションを抑制し、コールバックの生成頻度を下げます(たとえば、60 Hz の画面では 1 秒に 30 回)。これによりフレームレートは半分に低下しますが、アニメーションの一貫性は保たれます。前述のように、人間の目はフレームレートよりも変化に敏感になります。安定した 30 Hz は、1 秒に数フレームが欠落する 60 Hz よりも見栄えが良いです。

requestAnimationFrame については、すでに至るところで取り上げられています。これに関する詳細は、クリエイティブ JS のこちらの記事などを参照してくださいが、アニメーションをスムーズにするうえで重要な第一歩です。

フレーム バジェット

画面が更新されるたびに新しいフレームを準備する必要があるため、新しいフレームを作成するためのすべての作業を行えるのは、更新の合間に時間だけです。60 Hz のディスプレイでは、JavaScript の実行、レイアウトの実行、ペイントなど、ブラウザがフレームを取り出すためにしなければならないすべての処理の実行に約 16 ms かかります。つまり、requestAnimationFrame コールバック内の JavaScript の実行に 16 ミリ秒以上かかる場合、v-sync でフレームを時間内に生成することは望めません。

16 ミリ秒は短い時間です。幸いなことに、Chrome のデベロッパー ツールは、requestAnimationFrame コールバック中にフレーム バジェットを使い切っているかどうかを追跡するのに役立ちます。

Dev Tools のタイムラインを開いて、このアニメーションを録画すると、アニメーション化する際に予算を超過していることがすぐにわかります。タイムラインを [フレーム] に切り替えて、次の動画を見てみましょう。

レイアウトが多すぎるデモ
レイアウトが多すぎるデモ

これらの requestAnimationFrame(rAF)コールバックに 200 ミリ秒以上かかっています。これは、16 ミリ秒ごとにフレームを 1 フレームずつ出すには桁違いに長いです。長い rAF コールバックの 1 つを開くと、内部で何が起こっているかがわかります。この場合は、多くのレイアウトがあります。

Paul さんの動画では、再レイアウトの具体的な原因(scrollTop と読み上げられています)とその回避方法について詳しく説明しています。ここで重要なのは、コールバックの内容を調べて、時間がかかっている原因を調査できるということです。

レイアウトを大幅に縮小してデモを更新
レイアウトを大幅に縮小した最新のデモ

フレーム時間が 16 ミリ秒であることに注目してください。フレーム内の空白は、より多くの作業を行う(またはブラウザがバックグラウンドで行う必要がある)作業スペースを確保します。その空白スペースは良いことです。

その他のジャンク発生源

JavaScript によるアニメーションを実行しようとしたときに発生する最大の問題は、他の要因によって rAF コールバックが妨げられ、さらには実行できなくなる可能性があることです。rAF コールバックがリーンで、わずか数ミリ秒で実行されても、他のアクティビティ(到着したばかりの XHR の処理、入力イベント ハンドラの実行、タイマーでスケジュールされた更新の実行など)が突然到着し、時間を確保せずに実行される可能性があります。モバイル デバイスでは、このようなイベントの処理に数百ミリ秒かかることがあり、その間、アニメーションは完全に停止します。このようなアニメーション ヒッチをジャンクと呼びます。

このような状況を回避できる魔法の薬はありませんが、成功に向けて準備するためのアーキテクチャに関するベスト プラクティスがいくつかあります。

  • 入力ハンドラで大量の処理を行わないでください。たとえば、onscroll ハンドラで JavaScript を多用したり、ページ全体を再配置したりすると、ひどいジャンクが発生することがよくあります。
  • 可能な限り多くの処理(読み取り: 実行に時間がかかるもの)を rAF コールバックまたはウェブワーカーに push します。
  • rAF コールバックに作業をプッシュする場合は、各フレームを少しだけ処理するようにチャンク化するか、重要なアニメーションが終了するまで遅延させるようにします。これにより、短時間の rAF コールバックを引き続き実行し、スムーズにアニメーション化できます。

処理を入力ハンドラではなく requestAnimationFrame コールバックにプッシュする方法に関する優れたチュートリアルについては、Paul Lewis の記事 Leaner, Meaner, Faster Animations with requestAnimationFrame をご覧ください。

CSS アニメーション

イベントと rAF のコールバックにおける軽量の JS より優れている点は何ですか。JS なし。

先ほど、rAF コールバックの中断を回避するための特効薬はないと述べましたが、CSS アニメーションを使用すれば、こうした処理を完全に回避できます。特に Android 向け Chrome では(また、他のブラウザでも同様の機能の開発が進められています)、CSS アニメーションは非常に望ましいプロパティを持っており、JavaScript が実行中であってもブラウザで多くの場合に実行可能です。

上記のセクションには、ジャンクに関する暗黙的なステートメントがあります。ブラウザは一度に 1 つの処理しか実行できません。これは厳密には真実ではありませんが、ブラウザが JS の実行、レイアウトの実行、ペイントを実行することが常に可能だが、一度に 1 つしか実行できない、と想定することをおすすめします。これは、デベロッパー ツールのタイムライン ビューで確認できます。このルールの例外の 1 つが、Android 版 Chrome の CSS アニメーションです(現時点ではデスクトップ版 Chrome でも近日中にリリースされます)。

可能であれば、CSS アニメーションを使用することで、アプリケーションを簡素化し、JavaScript の実行中にアニメーションをスムーズに実行できます。

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

このボタンをクリックすると、JavaScript が 180 ミリ秒実行され、ジャンクが発生します。しかし、そのアニメーションを CSS アニメーションで動作させると、ジャンクは発生しなくなります。

(この記事の執筆時点では、CSS アニメーションは Android 版 Chrome でのみジャンクなしです。パソコンの Chrome では利用できません)。

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

CSS アニメーションの使用方法について詳しくは、MDN のこちらの記事などをご覧ください。

まとめ

要点は次のとおりです。

  1. アニメーション化するときは、画面が更新されるたびにフレームを生成することが重要になります。Vsync のアニメーションは、アプリの印象に大きくプラスの影響を与えます。
  2. Chrome や他の最新のブラウザで vsync によるアニメーションを表示するには、CSS アニメーションを使用することをおすすめします。CSS アニメーションよりも高い柔軟性が必要な場合は、requestAnimationFrame ベースのアニメーションをおすすめします。
  3. rAF アニメーションの正常な動作を維持するには、他のイベント ハンドラが rAF コールバックの実行を妨げないようにし、rAF コールバックを短く(15 ミリ秒未満)します。

最後に、vsync によるアニメーションは、シンプルな UI アニメーションだけでなく、Canvas2D アニメーション、WebGL アニメーション、静的ページのスクロールにも適用できます。このシリーズの次の記事では、これらのコンセプトを念頭に置いて、スクロールのパフォーマンスについて詳しく説明します。

今後ともどうぞよろしくお願いいたします。

参照