スキップしてメイン コンテンツに移動

JavaScriptのsetTimeoutやsetIntervalをrequestAnimationFrameで置き換える

JavaScriptで関数などを遅らせて実行する時間差の処理に使えるsetTimeout()メソッドと間隔を空けて繰り返せるsetInterval()メソッドには共通するパフォーマンスの問題があって避けるためにはどちらもrequestAnimationFrame()メソッドで置き換えることができる。

requestAnimationFrameはジャンクを防ぐ

アニメーションの処理に利点があってサイトの表示を良好に保ち易くするのがrequestAnimationFrame()メソッドの大きな特徴だ。

フレームワークまたはサンプルでは、アニメーションのような視覚変化を実現するために setTimeout または setInterval を使用する場合がありますが、これの問題はコールバックがフレームの任意の位置で(おそらくフレームの最後で)実行されることです。そのため、1 つのフレームが見落とされ、ジャンクを発生させることが頻繁にあります。

フレームの途中でJavaScriptが実行されてジャンクを引き起こす状況

現行のブラウザは一秒間に六十回のフレームの更新で描画するけど、アニメーションに関するプログラムはそれとタイミングを合わせるように連動して実行されないとフレーム飛ばしなどのジャンクという画面が乱れるような場合が出て来してしまうかも知れない。

取り分けレイアウトの変更はサイトで広く(最上要素から最下要素まで)影響する可能性があるらしく、例えばコンテンツの挿入などは特に注意しなくてはならないとされる。

JavaScriptのsetTimeout()メソッドや間隔を空けるsetInterval()メソッドも同様に画面のフレームの切り替えには構わず、設定された時間になるとコールバックのプログラムを実行する。

requestAnimationFrame()メソッドを使うと画面のフレームの切り替えに合わせた処理が可能だからアニメーションのサイトのスムーズな閲覧に結び付く。

requestAnimationFrameはイベントループに強い

もう一つsetTimeoutやsetIntervalにはイベントループに関して設定した時間通りに必ずしも動作しないという問題もある。

setTimeoutを呼び出すと、2番目の引数として渡された時間が経過した後、メッセージがキューに追加されます。キューに他のメッセージがない場合、メッセージはすぐに処理されます。しかしながら、メッセージがある場合、setTimeoutメッセージは他のメッセージを処理するために待機する必要があります。そのため第二引数は、保証時間ではなく、最小の時間を示しています。

並行モデルとイベントループ|MDN

もしも他の処理が先にあれば設定した時間よりも遅れる可能性が出て来るから注意しなくてはならないだろう。

イベントループの他にもブラウザの状況によって遅延が指定値より長い理由が幾つか挙げられるようだ。

requestAnimationFrameだと何よりも画面のフレームの更新に合わせて実行されるので、setTimeoutやsetIntervalと比べてイベントループにもっと強いと考えられる。

アニメーションの始動と時間通りの実行に向く

setTimeoutやsetIntervalにはパフォーマンスを低下させ得る二つの問題が含まれているけれどもこれらを一挙に解決し得るのがrequestAnimationFrameによる置き換えなんだ。

requestAnimationFrame()メソッドを使うとサイトのアニメーションの開始をブラウザのフレームの更新に合わせることができる。

このメソッドは、いつでも画面上でアニメーションの更新準備が整った時に呼び出してください。これにより、ブラウザの次の再描画が実行される前にアニメーション関数が呼び出されることを要求します。このコールバックの回数は、たいてい毎秒 60 回ですが、一般的に多くのブラウザーでは W3C の勧告に従って、ディスプレイのリフレッシュレートに合わせて行われます。ただし、コールバックの確率は、バックグラウンドのタブや隠れた <iframe> では、パフォーマンス向上やバッテリー消費を減らすために低くなるでしょう。

Window.requestAnimationFrame()|MDN

パフォーマンスの利点としてサイトを閲覧する間に画面が乱れ難いだけではなくて閉じてタブに置いておく間にも描画するフレームを減らすなどの動作の縮小によるデバイスの節電効果もあるらしい。

requestAnimationFrame()メソッドは時間に関してはそれ自体に組み込まれているわけではない。一秒間に六十回のフレーム更新に合わせるだけなので、そのままでは望んだ時間から速くも遅くも動作することになる。なのでsetTimeout()メソッドやsetInterval()メソッドの代わりに時間を設定して使うには幾らか加工しなくてはならない。

幸い、きっかり合わせることが可能で、すると少なくとも気がかりなイベントループによる遅れも免れるから使わない手はないと喜ぶんだ。

requestAnimationFrameに時間を設定して使う

サイトでの経過時間をDateコンストラクターからgetTime()メソッドで取得してrequestAnimationFrame()メソッドのフレームに対する動作を条件付ける。

二秒後に実行して即座に停止するサンプル

二秒後に出現:

JavaScript

let cancel;
function use() {
const start = new Date().getTime();
function raf() {
const progress = new Date().getTime() - start;
if (progress >= 2000) {
document.querySelector("span.panda").style.visibility = "visible"; return; }
cancel = requestAnimationFrame(raf); }
requestAnimationFrame(raf); }

※帯文字は遅らせて実行したい処理の書き込み。

二つの関数で構成されていてrequestAnimationFrameを使うものとそれを時間通りに実行させるための開始時間を取得するものに分かれる。サンプルでは前者のrafが後者のuseの中に入っている。useで得た開始時間をrafの進行時間から差し引いてフレームに合わせた実行時間を算出して条件分岐から所定の遅らせたいプログラムの実行に持って行く。

requestAnimationFrameの基本的な用法だと次のフレームに合わせて実行されるけれども時間を設定する場合は次の次のフレームから実行しなくてはならない。それまで待ちながら反復して使用するためにコールバックの中にも置いて再帰的に実行するようにプログラムする。サンプルでは一番下のrequestAnimationFrame(raf)で発火して初回の実行に入り、反復する二回目の実行からはraf()の内側に記載した初回と同じものによって設定した時間まで繰り返して発火し続けることになる。

requestAnimationFrameは一秒間に六十回のフレームを検出するからブラウザによって挙動は異なるようだけれども最短で0.016秒程度の間隔を持つ。だから二回目以降のフレームは最速でも0.032秒程度以降を想定することになり、非常に短いから実用上は殆ど気にする必要はないかも知れないけど、とにかく時間を設定するときも念頭に置いておくと良いと思う。

動作には僅かでもフレームの更新の間隔があって時間に合わせられるといっても完全に時間通りに実行するプログラムではないわけで、かりに検出される二回目のフレームの0.032秒程度以降を想定するのでなければrequestAnimationFrameを反復して使用する意味はないし、場合によってプログラムのバグの原因ともなり兼ねない。

Ifの条件文の数字が遅らせる時間になる。サンプルはミリ秒の単位で設定する。requestAnimationFrameはフレームの更新に合わせて関数を実行するので、実際は設定した時間ではなくて僅かに遅れた直後のフレームから処理を行うことになる。

一秒間隔で実行して十秒で停止するサンプル

一秒間隔で五回点滅:🐬

JavaScript

let cancel;
function use() {
const start = new Date().getTime();
let i = 1; 
function raf() {
const progress = new Date().getTime() - start;
if (progress >= 1000 * i) {
i++; document.querySelector("span.dolphin").style.visibility = "hidden"; } else if (progress >= 10000) {
document.querySelector("span.dolphin").style.visibility = "visible"; return; } else if (i % 2 !== 0) {
document.querySelector("span.dolphin").style.visibility = "visible"; }
cancel = requestAnimationFrame(raf); }
requestAnimationFrame(raf); }

※帯文字は間隔を空けて実行したい処理の書き込み。

setTimeout()メソッドの遅延処理とsetInterval()メソッドの反復処理で条件付けに関してプログラムが幾らか異なる。前者は時間を取得する幾つかの変数と一つの条件文を追加して可能だけれども後者は間隔を空けて繰り返すための回数を捉える変数が追加される。サンプルのiで、三ヵ所に記載して、何回、間隔を空ける条件文を通過したかで次の間隔を算出している。そして最初の条件文の数字が間隔で、二番目の数字が限界の時間になる。限界の数字の条件文を抜くと一定の間隔で止まらずに繰り返し続けることになる。

どちらのプログラムでも一定の時間が来ると終了するように条件分岐の実行文に遅らせる処理と共にreturnも記載して抜け出すようにしてある。

requestAnimationFrameを止めるためのcancelAnimationFrameメソッドには当該のrequestAnimationFrame()メソッドの返値が必要で、変数のcancelを用意して二回目以降の発火と同時に格納している。スイッチなどに使って止めることができる。または止めないか条件文で抜ける以外に不要ならばrequestAnimationFrameの返値は取得しなくても構わない。

参考:ウェブアプリのパフォーマンスの問題を特定して修正する Window.requestAnimationFrame() - 60fpsの間隔で繰り返し処理を実行する Implementing setTimeout using requestAnimationFrame

コメント

些細な日常の人気の投稿

飽和脂肪酸の多いココナッツオイルの過剰摂取の危険性とその他の健康上の利点

イメージ

PlayストアでAndroidアプリのダウンロードが非常に遅い場合の打開策

イメージ

早川愛の高校野球の夏の甲子園の大会歌の栄冠は君に輝くの独唱のソプラノの美声

イメージ