はじめに
ウェブアプリでアニメーション、遷移、その他の小さな UI 効果を実行する際に、レスポンシブでスムーズな操作性を実現したいと考えています。これらの効果をジャンクフリーにすることで、「ネイティブ」な操作感と、ぎこちなく洗練されていない操作感の違いが生まれます。
この記事は、ブラウザでのレンダリング パフォーマンスの最適化に関するシリーズの第 1 回目です。まず、スムーズなアニメーションが難しい理由と、それを実現するために必要なこと、そして簡単なベスト プラクティスをいくつかご紹介します。これらのアイデアの多くは、今年、Nat Duca と私が Google I/O で行った講演「Jank Busters」(動画)で最初に発表されたものです。
V-sync の概要
PC ゲーマーにはおなじみの用語かもしれませんが、ウェブではあまり使われていません。V-Sync とは何でしょうか。
スマートフォンのディスプレイを考えてみましょう。ディスプレイは一定の間隔で更新されます。通常は 1 秒間に約 60 回ですが、必ずしもそうとは限りません。V-sync(垂直同期)とは、画面の更新時にのみ新しいフレームを生成する手法です。これは、画面バッファにデータを書き込むプロセスと、そのデータを読み取ってディスプレイに表示するオペレーティング システムとの間の競合状態のようなものです。バッファリングされたフレームの内容は、リフレッシュ中ではなく、リフレッシュの合間に変更する必要があります。そうしないと、モニターに 1 つのフレームの半分と別のフレームの半分が表示され、「テアリング」が発生します。
スムーズなアニメーションを実現するには、画面が更新されるたびに新しいフレームを準備する必要があります。これには、フレーム時間(フレームをいつまでに準備する必要があるか)とフレームの予算(ブラウザがフレームを生成するのにかかる時間)という 2 つの大きな意味があります。フレームを完成させるには、画面の更新間隔(60 Hz の画面で約 16 ミリ秒)しかありません。また、最後のフレームが画面に表示されたらすぐに、次のフレームの作成を開始する必要があります。
タイミングがすべて: requestAnimationFrame
多くのウェブ デベロッパーは、16 ミリ秒ごとに setInterval または setTimeout を使用してアニメーションを作成しています。これはさまざまな理由から問題となります(詳しくは後ほど説明します)が、特に懸念されるのは次の点です。
- JavaScript のタイマーの解像度は数ミリ秒単位
- リフレッシュ レートはデバイスによって異なる
前述のフレーム時間の問題を思い出してください。次の画面更新が発生する前に、JavaScript、DOM 操作、レイアウト、ペイントなど、すべての処理が完了したアニメーション フレームを準備する必要があります。タイマーの解像度が低いと、次の画面更新の前にアニメーション フレームを完了することが難しくなりますが、画面リフレッシュ レートが変動すると、固定タイマーでは不可能になります。タイマーの間隔がどのようなものであっても、フレームのタイミング ウィンドウから徐々に外れていき、最終的にはフレームがドロップされます。これは、タイマーがミリ秒単位の精度で起動した場合でも発生します。実際には、タイマーの精度はマシンがバッテリー駆動か電源に接続されているかによって異なり、バックグラウンド タブがリソースを占有している場合などにも影響を受けるため、ミリ秒単位の精度で起動することはありません(デベロッパーが発見したとおりです)。これがまれなケース(たとえば、1 ミリ秒のずれにより 16 フレームごとに発生する)であっても、1 秒間に数フレームがドロップされることに気づくでしょう。また、表示されないフレームを生成する作業も行うことになり、電力と CPU 時間を浪費することになります。この電力と CPU 時間は、アプリで他の処理を行うために使用できます。
ディスプレイによってリフレッシュ レートは異なります。60 Hz が一般的ですが、一部のスマートフォンは 59 Hz、一部のノートパソコンは低電力モードで 50 Hz に低下し、一部のデスクトップ モニターは 70 Hz です。
レンダリング パフォーマンスについて議論する際、フレーム/秒(FPS)に注目しがちですが、ばらつきはさらに大きな問題になる可能性があります。人間の目は、タイミングの悪いアニメーションが引き起こす、アニメーションの小さな不規則な引っかかりに気づきます。
アニメーション フレームのタイミングを正しく設定するには、requestAnimationFrame を使用します。この API を使用すると、ブラウザにアニメーション フレームをリクエストすることになります。ブラウザが新しいフレームを生成する直前に、コールバックが呼び出されます。これは、リフレッシュ レートに関係なく発生します。
requestAnimationFrame には、他にも次のような優れた特性があります。
- バックグラウンド タブのアニメーションが一時停止され、システム リソースとバッテリー駆動時間が節約されます。
- システムが画面のリフレッシュ レートでレンダリングを処理できない場合、アニメーションをスロットルして、コールバックの頻度を減らすことができます(たとえば、60 Hz の画面で 1 秒間に 30 回)。これにより、フレームレートは半分になりますが、アニメーションの一貫性は維持されます。前述のように、人間の目はフレームレートよりもばらつきに敏感です。安定した 30 Hz は、1 秒間に数フレームが欠落する 60 Hz よりも見栄えがよくなります。
requestAnimationFrame については、すでにさまざまなところで議論されています。詳しくは、Creative JS のこちらの記事などをご覧ください。ただし、これはスムーズなアニメーションを実現するための重要な最初のステップです。
フレーム予算
画面が更新されるたびに新しいフレームを準備する必要があるため、新しいフレームを作成するすべての作業を更新と更新の間に行う必要があります。60 Hz のディスプレイでは、すべての JavaScript の実行、レイアウト、ペイント、その他ブラウザがフレームを出力するために必要な処理をすべて行うのに約 16 ミリ秒かかることになります。つまり、requestAnimationFrame コールバック内の JavaScript の実行に 16 ミリ秒以上かかると、v-sync に間に合うようにフレームを生成することはできません。
16 ミリ秒は長い時間ではありません。幸いなことに、Chrome のデベロッパー ツールを使用すると、requestAnimationFrame コールバック中にフレーム予算を超過しているかどうかを追跡できます。
DevTools のタイムラインを開き、このアニメーションの動作を記録すると、アニメーション化の際に予算を大幅に超過していることがすぐにわかります。[Timeline] で [Frames] に切り替えて、確認します。
これらの requestAnimationFrame(rAF)コールバックには 200 ミリ秒以上かかっています。16 ミリ秒ごとにフレームを刻むには、1 桁長すぎます。長い rAF コールバックの 1 つを開くと、内部で何が起こっているかがわかります。この場合は、多くのレイアウトです。
Paul の動画では、再レイアウトの具体的な原因(scrollTop の読み取り)と、それを回避する方法について詳しく説明しています。ここで重要なのは、コールバックを詳しく調べて、何に時間がかかっているのかを調査できるということです。
フレーム時間が 16 ミリ秒であることに注目してください。フレーム内の空白スペースは、より多くの作業を行う(またはブラウザがバックグラウンドで必要な作業を行う)ためのヘッドルームです。この空白スペースは、
その他のジャンク発生源
JavaScript を利用したアニメーションを実行しようとする際に発生する問題の最大の原因は、他の処理が rAF コールバックの邪魔をしたり、実行を完全に妨げたりすることです。rAF コールバックが軽量で、数ミリ秒で実行される場合でも、他のアクティビティ(受信した XHR の処理、入力イベント ハンドラの実行、タイマーでのスケジュールされた更新の実行など)が突然発生し、一定期間実行されることがあります。モバイル デバイスでは、これらのイベントの処理に数百ミリ秒かかることがあり、その間、アニメーションは完全に停止します。このようなアニメーションの不具合を「ジャンク」と呼びます。
このような状況を回避するための特効薬はありませんが、成功に導くためのアーキテクチャのベスト プラクティスがいくつかあります。
- 入力ハンドラで大量の処理を行わないでください。たとえば、onscroll ハンドラで大量の JS を実行したり、ページ全体を再配置しようとしたりすると、非常に大きなジャンクが発生する原因になります。
- できるだけ多くの処理(実行に時間がかかるもの)を rAF コールバックまたは Web Worker にプッシュします。
- rAF コールバックに処理をプッシュする場合は、各フレームで少しずつ処理するように分割するか、重要なアニメーションが終了するまで遅らせるようにします。これにより、短い rAF コールバックを実行し続け、スムーズにアニメーション化できます。
入力ハンドラではなく requestAnimationFrame コールバックに処理をプッシュする方法を説明した優れたチュートリアルとして、Paul Lewis 氏の記事 Leaner, Meaner, Faster Animations with requestAnimationFrame をご覧ください。
CSS アニメーション
イベントや rAF コールバックで軽量の JS を使用するよりも優れた方法があります。JavaScript は使用しません。
先ほど、rAF コールバックの中断を回避するための特効薬はないと述べましたが、CSS アニメーションを使用すれば、rAF コールバックの必要性を完全に回避できます。特に Chrome for Android では(他のブラウザでも同様の機能が開発されています)、JavaScript が実行されている場合でも、ブラウザで CSS アニメーションを実行できるという非常に望ましい特性があります。
ジャンクに関する上記のセクションには、ブラウザは一度に 1 つのことしかできないという暗黙のステートメントがあります。これは厳密には正しくありませんが、作業上の前提として、ブラウザは任意の時点で JS の実行、レイアウトの実行、ペイントのいずれかを行うことができますが、一度に 1 つしか実行できないと考えるのが適切です。これは、DevTools のタイムライン ビューで確認できます。このルールの例外の 1 つは、Chrome for Android(および、まだ対応していませんが、まもなくデスクトップ版 Chrome)の CSS アニメーションです。
可能な場合は、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 のこちらの記事などをご覧ください。
まとめ
簡単に言うと、次のようになります。
- アニメーション化する場合、画面の更新ごとにフレームを生成することが重要です。Vsync されたアニメーションは、アプリの操作感に大きなプラスの影響を与えます。
- Chrome やその他の最新のブラウザで VSYNC されたアニメーションを取得する最善の方法は、CSS アニメーションを使用することです。CSS アニメーションよりも柔軟性が必要な場合は、requestAnimationFrame ベースのアニメーションが最適な手法です。
- rAF アニメーションを正常に動作させるには、他のイベント ハンドラが rAF コールバックの実行を妨げないようにし、rAF コールバックを短く(15 ミリ秒未満)保ちます。
最後に、vsync されたアニメーションは、シンプルな UI アニメーションだけでなく、Canvas2D アニメーション、WebGL アニメーション、静的ページでのスクロールにも適用されます。このシリーズの次の記事では、これらのコンセプトを念頭に置いて、スクロールのパフォーマンスについて詳しく説明します。
ぜひご活用ください。