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