requestIdleCallback の使用

多くのサイトやアプリには、実行するスクリプトが多数含まれています。JavaScript は、多くの場合、できるだけ早く実行する必要がありますが、同時にユーザーの邪魔にならないようにする必要があります。ユーザーがページをスクロールしているときに分析データを送信したり、ユーザーがたまたまボタンをタップしているときに要素を DOM に追加したりすると、ウェブアプリが応答しなくなり、ユーザー エクスペリエンスが低下する可能性があります。

requestIdleCallback を使用して必須でない処理をスケジュールする

そこで役立つのが requestIdleCallback API です。requestAnimationFrame を採用することで、アニメーションを適切にスケジュールし、60 fps に達する可能性を最大限に高めることができます。同様に、requestIdleCallback は、フレームの最後に空き時間があるときやユーザーがアクティブでないときに処理をスケジュールします。つまり、ユーザーの邪魔にならずに作業を進める機会があるということです。Chrome 47 以降で利用できるため、すぐに Chrome Canary をお試しいただけます。これは試験運用版の機能であり、仕様はまだ変化していないため、今後変更される可能性があります。

requestIdleCallback を使用する理由

重要でない処理を自身でスケジュールすることは非常に困難です。requestAnimationFrame コールバックの実行後、スタイル計算、レイアウト、ペイント、その他のブラウザ内部の実行が必要になるため、残りのフレーム時間を正確に把握することはできません。ホームロール ソリューションでは、これらを考慮することはできません。ユーザーがなんらかの方法でインタラクションしていないことを確認するため、機能上必要ではない場合でも、あらゆる種類のインタラクション イベント(scrolltouchclick)にリスナーをアタッチする必要があります。一方、ブラウザはフレームの終了時点の残り時間、およびユーザーが操作しているかどうかを正確に把握しているため、requestIdleCallback を通じて、可能な限り最も効率的な方法で空き時間を利用できる API を取得できます。

もう少し詳しく見て、活用する方法を見てみましょう。

requestIdleCallback の確認

requestIdleCallback はまだ初期段階なので、利用する前に、利用可能かどうかをご確認ください。

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

その動作を shim することもできますが、その場合は setTimeout にフォールバックする必要があります。

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

setTimeout の使用は、requestIdleCallback のようにアイドル時間がわからないためあまり好ましくありませんが、requestIdleCallback が利用できない場合は関数を直接呼び出すため、この方法でも問題はありません。shim を使用すると、requestIdleCallback が使用可能になると、呼び出しが通知なくリダイレクトされます。これは素晴らしいことです。

ただし、ここでは、このエンティティが存在すると仮定します。

requestIdleCallback の使用

requestIdleCallback の呼び出しは、最初のパラメータとしてコールバック関数を受け取るという点で、requestAnimationFrame とよく似ています。

requestIdleCallback(myNonEssentialWork);

myNonEssentialWork が呼び出されると、deadline オブジェクトに渡されます。このオブジェクトには、処理の残り時間を示す数値を返す関数が含まれています。

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

timeRemaining 関数を呼び出すと、最新の値を取得できます。timeRemaining() がゼロを返したときに、さらにやるべきことが残っている場合は、別の requestIdleCallback をスケジュールできます。

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

関数が呼び出されることを保証する

本当に忙しいときはどうしますか?コールバックがまったく呼び出されないことに不安を感じるかもしれません。requestIdleCallbackrequestAnimationFrame に似ていますが、オプションの 2 番目のパラメータ(timeout プロパティを含むオプション オブジェクト)を受け取る点も異なります。このタイムアウトが設定されている場合、ブラウザはコールバックの実行に必要な時間をミリ秒単位で指定します。

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

タイムアウトの呼び出しが原因でコールバックが実行される場合は、次の 2 つの点に気付きます。

  • timeRemaining() はゼロを返します。
  • deadline オブジェクトの didTimeout プロパティは true になります。

didTimeout が true であれば、処理を実行してやりたいだけだと考えられます。

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

このタイムアウトはユーザーに支障をきたす可能性があるため(処理によってアプリが応答しなくなったり、ジャンクになったりする可能性があります)このパラメータの設定には注意が必要です。可能であれば、コールバックを呼び出すタイミングはブラウザに任せます。

requestIdleCallback を使用して分析データを送信する

requestIdleCallback を使用して分析データを送信する方法を見てみましょう。この場合、たとえばナビゲーション メニューをタップするなどのイベントをトラッキングします。ただし、通常は画面上にアニメーション表示されるため、このイベントはすぐには Google アナリティクスに送信されません。送信するイベントの配列を作成し、今後のある時点で送信するようリクエストします。

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

次に、requestIdleCallback を使用して保留中のイベントを処理する必要があります。

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

ここではタイムアウトを 2 秒に設定しましたが、この値はアプリケーションによって異なります。分析データの場合、将来の特定の時点ではなく妥当な期間内にデータが報告されるように、タイムアウトを使用することは理にかなっています。

最後に、requestIdleCallback が実行する関数を記述する必要があります。

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

この例では、requestIdleCallback が存在しない場合はアナリティクス データを直ちに送信する必要があると仮定しました。ただし、本番環境のアプリケーションでは、通信と競合してジャンクが発生しないように、タイムアウトを設定して送信を遅らせる方がよいでしょう。

requestIdleCallback を使用して DOM を変更する

requestIdleCallback がパフォーマンスの向上に役立つもう一つの状況は、必要のない DOM 変更が必要な場合です。たとえば、増え続ける遅延読み込みリストの最後にアイテムを追加する場合などです。requestIdleCallback が一般的なフレームにどのように適合するかを見てみましょう。

典型的なフレーム。

ブラウザがビジー状態で特定のフレーム内のコールバックを実行できない可能性があるため、フレームの最後に追加の作業を行うための自由時間があることを想定しないでください。この点で、フレームごとに実行される setImmediate などと異なります。

コールバックがフレームの最後で呼び出された場合、現在のフレームが commit された後にスケジュールされます。つまり、スタイルの変更が適用され、重要な点として、レイアウトが計算されたことになります。アイドル コールバック内で DOM を変更すると、それらのレイアウト計算は無効になります。次のフレームでなんらかのレイアウトの読み取り(getBoundingClientRectclientWidth など)が発生すると、ブラウザは強制同期レイアウトを実行しなければならず、これがパフォーマンスのボトルネックになる可能性があります。

アイドル状態のコールバックで DOM の変更がトリガーされないもう 1 つの理由は、DOM の変更による時間的影響が予測不可能であり、ブラウザが設定した期限を簡単に過ぎてしまうためです。

DOM の変更は、このタイプの処理を考慮してブラウザでスケジュールされるため、requestAnimationFrame コールバック内でのみ行うことをおすすめします。つまり、コードではドキュメント フラグメントを使用する必要があります。ドキュメント フラグメントは、次の requestAnimationFrame コールバックに追加できます。VDOM ライブラリを使用している場合、requestIdleCallback を使用して変更を加えますが、アイドル状態のコールバックではなく、次の requestAnimationFrame コールバックで DOM パッチを適用します。

それを念頭に置いて、コードを見てみましょう。

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

ここでは、要素を作成し、textContent プロパティを使用してその要素を入力しますが、要素の作成コードの方が複雑になる可能性はあります。要素を作成した後、scheduleVisualUpdateIfNeeded が呼び出されます。これにより、単一の requestAnimationFrame コールバックが設定され、本文にドキュメント フラグメントが追加されます。

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

問題なく DOM にアイテムを追加する際に、ジャンクが大幅に減少します。うまくできました。

よくある質問

  • ポリフィルはありますか? 残念ながらその必要はありませんが、setTimeout への透過的なリダイレクトが必要な場合は shim があります。この API が存在する理由は、この API がウェブ プラットフォームに実質的なギャップを埋めるためです。アクティビティがないことを推測するのは困難ですが、フレームの終了時の空き時間を特定する JavaScript API が存在しないため、可能な限り推測する必要があります。setTimeoutsetIntervalsetImmediate などの API を使用して処理のスケジュールを設定できますが、requestIdleCallback のようなユーザー操作を回避するタイミングは設定されません。
  • 期限を過ぎた場合はどうなりますか? timeRemaining() がゼロを返しても、実行時間の延長を選択した場合、ブラウザで処理が停止するのを心配せずに実行することができます。ただし、ブラウザにはユーザーにスムーズなエクスペリエンスを提供するための期限が設けられているため、正当な理由がない限り、常に期限を遵守する必要があります。
  • timeRemaining() が返す最大値はありますか?はい、現在は 50 ミリ秒です。レスポンシブ アプリケーションを維持するには、ユーザー操作に対するすべてのレスポンスを 100 ミリ秒未満にする必要があります。ユーザーが 50 ミリ秒の時間枠内で操作した場合は、ほとんどの場合、アイドル状態のコールバックが完了してブラウザがユーザーの操作に応答するのを待つ必要があります。アイドル状態のコールバックが連続してスケジュールされることがあります(ブラウザが実行するのに十分な時間があると判断した場合)。
  • requestIdleCallback で行ってはいけない処理はありますか? 理想的には、比較的予測可能な特性を持つ小さなまとまり(マイクロタスク)で作業を行います。たとえば、特に DOM を変更すると、スタイルの計算、レイアウト、ペイント、合成がトリガーされるため、実行時間が予測できません。そのため、上記のように DOM の変更は requestAnimationFrame コールバックのみで行う必要があります。もう 1 つ注意すべき点は、Promise を解決(または拒否)することです。残りの時間がなくても、アイドル状態のコールバックが終了すると、コールバックはすぐに実行されます。
  • フレームの終了時に必ず requestIdleCallback が返されますか?いいえ、常にそうとは限りません。ブラウザは、フレームの終わりに空いている時間がある場合、またはユーザーがアクティブでない期間にコールバックをスケジュールします。コールバックがフレームごとに呼び出されることを想定すべきではありません。また、一定の時間内に実行する必要がある場合は、タイムアウトを利用する必要があります。
  • 複数の requestIdleCallback コールバックを使用できますか?はい、可能です。複数の requestAnimationFrame コールバックを使用できます。ただし、最初のコールバックでコールバックの残り時間が使い果たされた場合、他のコールバックに充てる時間がなくなることを覚えておいてください。他のコールバックは、ブラウザが次にアイドル状態になるまで待ってから実行する必要があります。処理する処理によっては、アイドル状態のコールバックを 1 つ作成し、そこで処理を分割することをおすすめします。または、タイムアウトを利用して、コールバックが時間切れにならないようにすることもできます。
  • 別のアイドル コールバックを内部で新しいコールバックとして設定するとどうなりますか? 新しいアイドル状態のコールバックは、(現在のフレームではなく)次のフレームからできるだけ早く実行されるようにスケジュールされます。

アイドル状態に!

requestIdleCallback は、ユーザーの邪魔にならないようにコードを確実に実行できる優れた方法です。使いやすく、柔軟性に優れています。ただし、まだ初期の段階であり、仕様が完全には確定しているわけではありません。フィードバックをお寄せいただければ幸いです。

Chrome Canary をぜひお試しください。ご自身のプロジェクトにぜひお役立てください。