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

Tom Wiltzius
Tom Wiltzius

はじめに

ウェブアプリで、アニメーション、遷移、その他の小さな UI 効果を行う際に、応答性と滑らかさを感じられるようにする必要があります。これらのエフェクトをジャンクのないものにすることで、「ネイティブ」なエフェクトと洗練されていないものだと感じるかもしれません。

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

V-sync のご紹介

PC ゲーマーにはこの用語に馴染みがあるかもしれませんが、ウェブでは「v-sync とは何なのか?」という言葉はあまり見られません。

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

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

タイミングがすべて: requestAnimationFrame

多くのウェブ デベロッパーは、アニメーションを作成するために 16 ミリ秒ごとに setInterval または setTimeout を使用します。この問題にはさまざまな理由があります(これについてはこの後で詳しく説明します)が、特に以下の点が懸念されます。

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

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

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

レンダリング パフォーマンスについて議論するときは 1 秒あたりのフレーム数(FPS)に重点を置く傾向がありますが、ばらつきはさらに大きな問題になり得ます。タイミングの悪いアニメーションでは、アニメーション内に小さな不規則な途切れが生じることがあります。

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

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

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

requestAnimationFrame についてはすでにさまざまなところで説明されています。詳しくは、Creative JS の記事などをご覧ください。ただし、これはアニメーションを滑らかにする重要な第一歩です。

フレーム バジェット

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

16 ミリ秒ではそれほど時間はかかりません。幸いなことに、Chrome のデベロッパー ツールを使用すると、requestAnimationFrame コールバック中にフレーム バジェットを使い果たしているかどうかを把握できます。

Dev Tools のタイムラインを開いて、このアニメーションの動作を録画すると、アニメーション化の際に予算をはるかに超えていることがわかります。タイムラインで [フレーム] に切り替えますご覧ください。

<ph type="x-smartling-placeholder">
</ph> レイアウトが多すぎるデモ
レイアウトが多すぎるデモ

これらの requestAnimationFrame(rAF)コールバックの所要時間は 200 ミリ秒を超えています。これは、16 ミリ秒ごとに 1 つのフレームを消すには桁違いです。長い rAF コールバックの 1 つを開くと、内部で何が起こっているか、ここでは多数のレイアウトが明らかになります。

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

<ph type="x-smartling-placeholder">
</ph> 更新されたデモ(レイアウトを大幅に縮小)
レイアウトを大幅に縮小した最新のデモ

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

その他のジャンク発生源

JavaScript を利用したアニメーションを実行しようとしたときに発生する最大の原因は、 他の要素が rAF コールバックの妨げとなり 防ぐことができますrAF コールバックが無駄がなく、わずか数個で実行されるとしても、 ミリ秒、その他のアクティビティ(受信したばかりの XHR の処理、 入力イベント ハンドラの実行、タイマーでのスケジュール設定された更新の実行など)は、 突然受信して任意の期間にわたって実行され 結果が生成されるわけではありませんモバイルの場合 デバイスではこれらのイベントの処理に 数百ミリ秒、 その間はアニメーションが完全に停止しますこれを アニメーションによってジャンクが発生する。

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

  • 入力ハンドラでは大量の処理を行わないでください。たとえば、オンスクロール ハンドラは、ひどいジャンクを引き起こす一般的な原因です。
  • できるだけ多くの処理(実行に時間がかかるもの)を rAF コールバックまたはウェブワーカーに可能な限り push します。
  • rAF コールバックに処理をプッシュする場合は、各フレームを少しずつ処理するように分割するか、重要なアニメーションが終わるまで遅らせるようにします。このようにすることで、短い rAF コールバックを引き続き実行し、スムーズにアニメーション化できます。

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

CSS アニメーション

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

先ほど、rAF コールバックの中断を回避できる解決策はないと述べましたが、CSS アニメーションを使用すると、そのような必要性を完全に回避できます。特に Chrome for Android では(また、他のブラウザも同様の機能の開発に取り組んでいます)、CSS アニメーションには、JavaScript が実行されている場合でもブラウザがしばしば実行できるという非常に望ましい特性があります。

上記のセクションには、ジャンクに関する暗黙的な記述があります。ブラウザは一度に 1 つのことしか実行できません。これは厳密には真実ではありませんが、ブラウザは常に JS、レイアウト、ペイントを実行できるが、一度に 1 つだけ、ということを前提とすると実用的です。これは、デベロッパー ツールのタイムライン ビューで検証できます。このルールの例外の 1 つは、Chrome for Android の 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 アニメーションは Chrome for Android でのみジャンクのない動作であり、パソコンの 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 コールバックは 短い(15 ミリ秒未満)。

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

アニメーションを楽しみましょう!

参照