requestIdleCallback の使用

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

requestIdleCallback を使用して重要でない処理のスケジュールを設定する。

便利な API requestIdleCallback が追加されました。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);
    }

setTimeoutrequestIdleCallback のようにアイドル時間を知らないので、使用はおすすめしませんが、requestIdleCallback が使用できない場合は関数を直接呼び出すため、この方法でシミュレートしても問題ありません。シムを使用すると、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 番目のパラメータ(タイムアウト プロパティを含むオプション オブジェクト)をオプションで受け取るという点でも異なります。このタイムアウトが設定されている場合、ブラウザはコールバックを実行するまでのミリ秒単位の時間を指定します。

// 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 がパフォーマンスに大きく貢献するもう 1 つの状況は、DOM の変更が必須ではない場合です。たとえば、増え続ける遅延読み込みリストの末尾にアイテムを追加する場合などです。requestIdleCallback が一般的なフレームにどのように収まるかを見てみましょう。

一般的なフレーム。

ブラウザがビジー状態になり、特定のフレーム内でコールバックを実行できない場合もあるため、フレームの終了時に他の処理を行うための空き時間がまったくないとは想定しないでください。これは、フレームごとに実行される setImmediate とは異なります。

フレームの終了時にコールバックが発生すると、現在のフレームが commit された後に実行されるようにスケジュールされます。つまり、スタイルの変更が適用され、重要なのはレイアウトが計算されます。アイドル状態のコールバック内で DOM を変更すると、それらのレイアウト計算が無効になります。次のフレームに getBoundingClientRectclientWidth などのレイアウト読み取りがある場合、ブラウザは強制同期レイアウトを実行する必要があります。これはパフォーマンスのボトルネックになる可能性があります。

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

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

では、コードを見てみましょう。

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

アイドル状態オン!

requestIdleCallback は、ユーザーの操作を妨げることなく、コードを確実に実行できる優れた方法です。使いやすく、非常に柔軟性があります。まだ初期段階であり、仕様も完全に確定していないため、フィードバックをお待ちしております。

Chrome Canary でこの機能を試し、プロジェクトで使ってみて、フィードバックをお寄せください。