●requestAnimationFrameとは?
みなさん、Webサイトやアプリケーションでアニメーションを実装する際、どのような方法を使っていますか?setTimeoutやsetIntervalを使ったことがある方も多いと思います。
しかし、これらの方法ではブラウザの描画サイクルとうまく同期できず、パフォーマンスが低下してしまうことがあります。
そこで登場するのが、requestAnimationFrameというJavaScript APIです。
requestAnimationFrameを使うことで、ブラウザの描画サイクルに合わせてアニメーションを実行できるようになります。
つまり、画面の更新に合わせてコードが呼び出されるため、無駄なく効率的にアニメーションを描画できるのです。
○ブラウザの描画サイクルとの同期
ブラウザは一定の間隔で画面を更新しています。
この更新のタイミングに合わせてアニメーションを実行することが、スムーズな動きを実現するポイントです。
requestAnimationFrameは、まさにこの描画サイクルに同期してコールバック関数を呼び出してくれるのです。
コールバック関数内でアニメーションの次の状態を計算し、再度requestAnimationFrameを呼び出すことで、連続的なアニメーションが実現します。
setTimeoutやsetIntervalとは異なり、requestAnimationFrameはブラウザが描画を行うタイミングでコールバックを実行するため、無駄なく効率的です。
実際のコードを見てみましょう。
このように、requestAnimationFrameにコールバック関数を渡すことで、アニメーションループを作ることができます。
ブラウザの描画サイクルに合わせてanimate関数が呼び出され、スムーズなアニメーションが実現するのです。
○60fpsアニメーションの実現
ブラウザの描画サイクルは通常60fpsです。
つまり、1秒間に60回の描画が行われています。
requestAnimationFrameを使えば、この60fpsに合わせてアニメーションを実行できるため、滑らかで自然な動きを実現できます。
60fpsのアニメーションを維持するためには、1フレームの処理を16.67ミリ秒(1秒÷60フレーム)以内に収める必要があります。
requestAnimationFrameを使えば、この時間内に処理を完了させることができ、高いパフォーマンスを保つことができるのです。
●FPS制御の重要性
みなさん、アニメーションを実装する際、FPSという言葉を聞いたことがあるでしょうか?
FPSとは、1秒間に描画されるフレーム数(Frames Per Second)のことです。
FPSを制御することは、アニメーションのパフォーマンスを最適化する上で非常に重要なのです。
○パフォーマンスへの影響
FPSが低いと、アニメーションがカクつきや遅延が発生し、ユーザーエクスペリエンスが損なわれてしまいます。
逆に、FPSが高すぎると、必要以上にCPUやGPUに負荷がかかり、他のタスクに影響を与える可能性があります。
理想的なFPSは、ディスプレイのリフレッシュレートに合わせることです。
一般的なディスプレイは60Hzのリフレッシュレートを持っているため、60fpsが目標となります。
しかし、複雑なアニメーションや、モバイルデバイスでは、60fpsを維持するのが難しい場合もあります。
例えば、次のようなコードを考えてみましょう。
この例では、animate関数内で重い処理を行っています。
このような処理がある場合、1フレームの描画に時間がかかってしまい、FPSが低下してしまします。
結果として、アニメーションがカクつく原因となるのです。
したがって、FPSを適切に制御し、パフォーマンスを最適化することが重要だと言えます。
次の点に注意しましょう。
- 1フレームの処理は16.67ミリ秒以内に収める
- 重い処理は分割するか、別スレッドで実行する
- 不要な再描画は避ける
これらを意識することで、高いFPSを維持し、スムーズなアニメーションを実現できます。
○バッテリー消費の抑制
FPSの制御は、パフォーマンスだけでなく、バッテリー消費にも影響します。
特にモバイルデバイスでは、バッテリー寿命が重要な問題です。
高いFPSでアニメーションを実行すると、CPUやGPUに高い負荷がかかり、バッテリーを多く消費してしまいます。
必要以上にFPSを上げることは、バッテリー寿命を短くする原因となります。
したがって、アニメーションを実装する際は、デバイスの性能や用途に合わせて適切なFPSを設定することが大切です。
例えば、モバイルデバイスでは30fpsで十分なケースもあります。
バッテリー消費を抑えるためのポイントは次の通りです。
- 必要以上にFPSを上げない
- アニメーションが不要な場面では、停止する
- レンダリングをオフスクリーンで行う
この工夫により、バッテリー消費を最小限に抑えながら、快適なアニメーションを提供できます。
それでは、いよいよFPS制御の具体的な方法について見ていきましょう。ここからはサンプルコードを交えながら、10個の手法を順番に解説していきます。
●FPS制御の10の方法
アニメーションのパフォーマンスを最適化するには、FPSを適切に制御することが重要だと前章でお伝えしましたね。
でも、具体的にはどうすればいいのでしょうか?ここからは、FPS制御の10の方法を1つずつ見ていきたいと思います。
○サンプルコード1:フレームレートの計算
まずは、フレームレートを計算する方法から始めましょう。
フレームレートを知ることは、FPS制御の第一歩です。
このコードでは、performance.now()を使って現在の時刻を取得し、1秒ごとにフレームレートを計算しています。
frameCount変数でフレーム数をカウントし、1秒経過するたびにコンソールにフレームレートを出力します。
実行すると、コンソールにフレームレートが表示されます。
これを確認することで、現在のアニメーションのパフォーマンスを把握できます。
○サンプルコード2:フレームレート制限
次に、フレームレートを制限する方法を見ていきましょう。
フレームレートを制限することで、必要以上にCPUやGPUに負荷をかけないようにできます。
このコードでは、targetFPS変数でターゲットのフレームレートを設定しています。
ここでは30fpsに設定していますが、状況に応じて調整してください。
frameInterval変数は、1フレームの間隔を計算しています。
animate関数内で、現在の時刻とlastTimeの差がframeIntervalより小さい場合、次のフレームをリクエストして関数を抜けます。
これにより、指定したフレームレートを超えないようにアニメーションが制御されます。
○サンプルコード3:時間ベースのアニメーション
3つ目は、時間ベースのアニメーションです。
これは、経過時間に基づいてアニメーションを更新する手法です。フレームレートに依存せず、一定の速度でアニメーションを進められます。
このコードでは、elapsed変数で前回のフレームからの経過時間を計算しています。この経過時間を使って、アニメーションの更新量を決定します。
例えば、speed変数で1秒間に100px移動するように設定し、distance変数で経過時間に応じた移動距離を計算しています。
この距離を使ってアニメーションを更新することで、フレームレートに関係なく一定の速度でアニメーションを進められます。
○サンプルコード4:デルタタイムの利用
4つ目は、デルタタイムを利用する方法です。
デルタタイムとは、前回のフレームからの経過時間のことです。
これを使うことで、フレームレートの変動に影響されないアニメーションを実装できます。
このコードは、サンプルコード3とよく似ていますね。ただし、elapsed変数の代わりにdeltaTime変数を使っています。
deltaTimeは秒単位の経過時間です。
例えば、speed変数で1秒間に100px移動するように設定し、distance変数でデルタタイムに応じた移動距離を計算しています。
これにより、フレームレートが変動しても一定の速度でアニメーションを進められます。
○サンプルコード5:重い処理の分割
次に、重い処理を分割する方法を見ていきましょう。
複雑な計算や大量のデータ処理などの重い処理があると、フレームレートが低下してしまいます。
そこで、処理を分割して複数のフレームに分けて実行することで、パフォーマンスを改善できます。
このコードでは、heavyTask関数内で重い処理を分割して実行しています。
totalCount変数で全体の処理数を設定し、chunkSize変数で1フレームで処理する数を設定します。
processChunk関数内で、chunkSizeの数だけ処理を実行し、currentIndexを更新します。
そして、currentIndexがtotalCountより小さい場合は、次のフレームでprocessChunkを呼び出して処理を継続します。
前回に引き続き、FPS制御の方法を見ていきましょう。ここからは、不要な再描画の防止やレンダリングの一時停止など、より高度なテクニックを紹介します。
○サンプルコード6:不要な再描画の防止
アニメーションのパフォーマンスを向上させるには、不要な再描画を避けることが重要です。
再描画が多いと、CPUやGPUに負荷がかかり、フレームレートが低下してしまいます。
このコードでは、isDirty変数を使って、再描画が必要かどうかを判断しています。
isDirtyがfalseの場合、画面を更新する処理をスキップします。
状態が変更された時だけ、isDirtyをtrueに設定することで、不要な再描画を防ぐことができます。
例えば、ユーザーの操作やデータの更新などがあった時に、onStateChange関数を呼び出してisDirtyをtrueにします。
これにより、変更があった時だけ画面が更新され、パフォーマンスの向上につながります。
○サンプルコード7:レンダリングの一時停止
時には、アニメーションを一時的に停止したい場合があります。
例えば、ブラウザのタブがバックグラウンドになった時や、ユーザーがページから離れた時などです。
そんな時は、レンダリングを一時停止することで、リソースを節約できます。
このコードでは、isRunning変数を使ってアニメーションの実行状態を管理しています。
pause関数でisRunningをfalseにすることで、アニメーションを一時停止します。
resume関数でisRunningをtrueに戻し、requestAnimationFrameを呼び出すことで、アニメーションを再開します。
また、visibilitychangeイベントを使って、ページの表示状態の変化を検出しています。
ページが非表示になった時にpause関数を呼び出し、再び表示された時にresume関数を呼び出すことで、自動的にアニメーションを一時停止・再開できます。
実行すると、ブラウザのタブがバックグラウンドになった時にアニメーションが一時停止し、再びフォアグラウンドになった時に再開される様子が確認できるはずです。
○サンプルコード8:requestIdleCallbackの活用
requestIdleCallbackは、ブラウザがアイドル状態になった時に呼び出されるコールバック関数を登録するためのAPIです。
これを使うことで、メインスレッドが忙しい時は処理を延期し、アイドル時に実行することができます。
このコードでは、requestIdleCallbackを使って、アイドル時に実行する処理を登録しています。
idleTask関数内では、deadline.timeRemaining()でアイドル期間の残り時間を取得し、その間に処理を実行します。
もし処理が完了しなかった場合は、thereIsMoreWork関数で判断し、再度requestIdleCallbackを呼び出して継続処理をリクエストします。
これにより、アニメーションの実行に影響を与えずに、バックグラウンドの処理を実行できます。
ただし、requestIdleCallbackはまだ全てのブラウザでサポートされている訳ではないので、使用する際は注意が必要です。
○サンプルコード9:OffscreenCanvasの利用
OffscreenCanvasは、メインスレッドとは別のスレッドでCanvasのレンダリングを行うためのAPIです。
これを使うことで、複雑な描画処理をバックグラウンドで実行し、メインスレッドのパフォーマンスを向上させることができます。
このコードでは、OffscreenCanvasを作成し、それをワーカースレッドに渡しています。
ワーカースレッド内では、渡されたOffscreenCanvasに対して描画処理を行います。
メインスレッドでは、requestAnimationFrameを使ってアニメーションを実行し、各フレームでOffscreenCanvasの内容をメインのCanvasにコピーしています。
実行すると、描画処理がワーカースレッドで行われるため、メインスレッドのパフォーマンスが向上し、より滑らかなアニメーションが実現できます。
ただし、OffscreenCanvasはまだ全てのブラウザでサポートされている訳ではないので、使用する際は注意が必要です。
○サンプルコード10:WebWorkersでの処理
WebWorkersを使うことで、重い処理をバックグラウンドで実行し、メインスレッドのパフォーマンスを向上させることができます。
アニメーションに関連する処理の一部をワーカースレッドに移動することで、より滑らかなアニメーションを実現できます。
このコードでは、メインスレッドでアニメーションを実行しつつ、重い処理をワーカースレッドに依頼しています。
ワーカースレッド内では、渡されたデータを使って重い処理を実行し、結果をメインスレッドに返します。
メインスレッドでは、ワーカースレッドから結果を受け取り、それを使ってアニメーションを更新します。
実行すると、重い処理がワーカースレッドで実行されるため、メインスレッドがブロックされることなく、滑らかなアニメーションが維持できます。
ただし、WebWorkersはメインスレッドとは別のコンテキストで実行されるため、DOM操作はできないなどの制限があることに注意してください。
●よくあるエラーと対処法
さて、ここまでrequestAnimationFrameを使ったFPS制御の方法を10個のサンプルコードを交えて解説してきました。
でも、実際にアニメーションを実装する際には、思わぬエラーに遭遇することもあるでしょう。
そこで、ここでは、よくあるエラーとその対処法を3つ紹介します。
これを理解することで、スムーズにアニメーションを実装できるようになるはずです。
○フレームレートが不安定になる
アニメーションを実装していると、フレームレートが不安定になることがあります。
特に、重い処理を実行している場合や、多くの要素を同時にアニメーションさせている場合に起こりやすいです。
フレームレートが不安定になる原因は、主に次の2つです。
- 1フレームの処理に時間がかかりすぎている
- メインスレッドがブロックされている
これらを解決するには、次のような対処法が有効です。
- 重い処理は分割して実行する(サンプルコード5参照)
- 不要な再描画は避ける(サンプルコード6参照)
- WebWorkersを活用して処理を分散する(サンプルコード10参照)
例えば、次のようなコードを考えてみましょう。
このコードでは、重い処理と多くの要素のアニメーションを1フレーム内で実行しています。
そのため、フレームレートが不安定になってしまいます。
これを改善するには、重い処理を分割し、WebWorkersを使って別スレッドで実行するのが効果的です。
また、アニメーションする要素を減らしたり、CSSアニメーションを使ったりすることで、パフォーマンスを向上させることができます。
○アニメーションがカクつく
フレームレートが安定していても、アニメーションがカクつくことがあります。
これは、次のような原因が考えられます。
- repaints(再描画)とreflows(リフロー)が多く発生している
- CSSアニメーションとJavaScriptアニメーションが混在している
- 非同期処理によってメインスレッドがブロックされている
これらを解決するには、次のような対処法が有効です。
- CSSアニメーションを使う
- will-change プロパティを使って最適化する
- 非同期処理はPromiseやasync/awaitを使う
例えば、JavaScriptでアニメーションを実装する代わりに、CSSアニメーションを使うことで、パフォーマンスを大幅に向上させることができます。
このように、CSSアニメーションを使えば、ブラウザがネイティブにアニメーションを処理してくれるため、JavaScriptによる制御よりも滑らかになります。
また、will-change プロパティを使って、アニメーションする要素を事前にブラウザに伝えておくことで、最適化することもできます。
これにより、ブラウザはその要素の変化に備えて最適化を行い、パフォーマンスが向上します。
○メモリリークが発生する
アニメーションを長時間実行していると、メモリリークが発生することがあります。
これは、不要になったオブジェクトがメモリから解放されずに蓄積していくことが原因です。
メモリリークが発生すると、アプリケーションのパフォーマンスが低下したり、最悪の場合はクラッシュしたりします。
メモリリークを防ぐには、次のような対処法が有効です。
- 不要になったイベントリスナーを解除する
- タイマーをクリアする
- 参照を適切に管理する
例えば、イベントリスナーを使っている場合は、不要になったタイミングで解除することが重要です。
同様に、setIntervalやsetTimeoutを使っている場合は、不要になったらclearIntervalやclearTimeoutを呼び出してタイマーをクリアしましょう。
また、オブジェクトへの参照を適切に管理することも大切です。
不要になったオブジェクトへの参照を残していると、ガベージコレクションの対象にならずにメモリリークの原因になります。
●requestAnimationFrameの応用例
これまで、requestAnimationFrameを使ったFPS制御の方法や、よくあるエラーと対処法について詳しく解説してきました。
でも、実際にアニメーションを実装する際には、どのようなユースケースがあるのでしょうか?
そこで、ここでは、requestAnimationFrameの応用例を4つ紹介します。実際のWebサイトやアプリケーションでよく見られる事例を通して、より実践的なテクニックを学びましょう。
○パララックススクロール
パララックススクロールは、スクロールに合わせて複数の層を異なる速度で動かすことで、奥行きのある効果を生み出すテクニックです。
Webサイトのヒーロー部分やストーリーテリングのセクションなどでよく使われます。
requestAnimationFrameを使って、スクロール位置に応じた要素の位置を計算し、滑らかにアニメーションさせることができます。
このコードでは、.parallaxクラスを持つ要素を取得し、それぞれの要素に設定されたdata-speed属性の値に基づいて、スクロール位置に応じた移動距離を計算しています。
そして、translateを使って要素を移動させることで、パララックス効果を実現しています。
実行すると、スクロールに合わせて要素が異なる速度で動き、奥行きのある表現が可能になります。
レイヤー構造を工夫することで、より印象的なデザインを作り出せるでしょう。
○インタラクティブな効果
Webサイトやアプリケーションに、インタラクティブな要素を取り入れることで、ユーザーエンゲージメントを高めることができます。
マウスの動きに反応するアニメーションや、スクロールに連動したエフェクトなどがその一例です。
requestAnimationFrameを使えば、これらのインタラクティブな効果をスムーズに実装できます。
このコードでは、マウスの動きに反応して要素を移動させるインタラクティブな効果を実装しています。
mousemoveイベントを監視し、マウスの座標を取得します。
そして、その座標に基づいて要素の位置を計算し、translate3dを使って移動させています。
requestAnimationFrameを使うことで、マウスの動きに合わせて滑らかにアニメーションさせることができます。
○ゲームループ
ゲーム開発においては、ゲームループと呼ばれる一連の処理を繰り返し実行することが基本となります。
ゲームの状態を更新し、描画するこの一連の流れを、requestAnimationFrameを使って制御することができます。
このコードは、シンプルなゲームループの実装例です。
gameLoop関数内で、ゲームの状態を更新するupdate関数と、描画処理を行うrender関数を呼び出しています。
そして、requestAnimationFrameを使って次のフレームでgameLoopを呼び出すことで、ループを継続します。
実際のゲーム開発では、この基本的な構造の上に、ゲームオブジェクトの管理や衝突判定、物理演算などの様々な処理が追加されます。
それぞれの処理を適切に設計し、パフォーマンスを最適化することが重要です。
○パフォーマンスモニタリング
最後に、requestAnimationFrameを使ったパフォーマンスモニタリングの方法を紹介します。
フレームレートやレンダリング時間を計測することで、アプリケーションのパフォーマンスをリアルタイムに可視化できます。
このコードでは、1秒ごとにフレームレートとレンダリング時間を計算し、コンソールに出力しています。
frameCount変数でフレーム数をカウントし、performance.nowを使って経過時間を計測します。
まとめ
さて、ここまでrequestAnimationFrameを使ったFPS制御の方法について、たくさんのサンプルコードを交えながら解説してきましたね。
requestAnimationFrameがブラウザの描画サイクルに同期し、60fpsのアニメーションを実現できることを理解していただけたかと思います。
これを理解し、自分のプロジェクトに応用することで、よりパフォーマンスの高いアニメーションを実現できるはずです。
でも、忘れないでほしいのは、最適化はゴールではなく、手段だということです。
大切なのは、ユーザーに価値を提供し、素晴らしい体験を届けること。
そのために、パフォーマンスという観点からアニメーションを最適化する。そんな姿勢でプロジェクトに取り組んでいただければと思います。