requestIdleCallback verwenden

Paul Lewis

Auf vielen Websites und in vielen Apps müssen viele Skripts ausgeführt werden. Ihr JavaScript muss häufig so schnell wie möglich ausgeführt werden. Gleichzeitig soll es den Nutzern aber nicht im Weg sein. Wenn Sie Analysedaten senden, wenn Nutzende auf der Seite scrollen, oder Sie Elemente an das DOM anhängen, während sie auf die Schaltfläche tippen, reagiert Ihre Webanwendung möglicherweise nicht mehr, was zu einer schlechten User Experience führt.

„requestIdleCallback“ zum Planen von unwichtigen Arbeiten verwenden.

Zum Glück gibt es jetzt eine passende API: requestIdleCallback. So wie wir dank requestAnimationFrame Animationen richtig planen und die Chancen auf 60 fps maximieren konnten, plant requestIdleCallback die Arbeit, wenn am Ende eines Frames kostenlose Zeit zur Verfügung steht oder wenn der Nutzer inaktiv ist. So haben Sie die Möglichkeit, Ihre Arbeit zu erledigen, ohne die Nutzenden zu stören. Die Funktion ist ab Chrome 47 verfügbar. Probieren Sie es also am besten gleich einmal mit Chrome Canary aus. Es handelt sich um eine experimentelle Funktion, deren Spezifikationen sich noch ändern, sodass sich die Dinge in Zukunft ändern können.

Warum sollte ich requestIdleCallback verwenden?

Es ist äußerst schwierig, unwesentliche Arbeiten selbst zu planen. Es ist unmöglich, genau zu sagen, wie viel Frame Time noch verbleibt, da nach der Ausführung von requestAnimationFrame-Callbacks Stilberechnungen, Layout, Farbe und andere Browser-Interna ausgeführt werden müssen. Mit einer Home-Roll-Lösung kann keiner dieser Faktoren berücksichtigt werden. Um sicherzustellen, dass ein Nutzer nicht auf irgendeine Weise interagiert, müssen Sie jeder Art von Interaktionsereignis (scroll, touch, click) auch Listener hinzufügen, auch wenn Sie sie für die Funktionalität nicht benötigen. Nur, damit Sie sicher sein können, dass der Nutzer nicht interagiert. Der Browser weiß hingegen genau, wie viel Zeit am Ende des Frames zur Verfügung steht und ob der Nutzer mit der Anzeige interagiert. Deshalb erhalten wir mit requestIdleCallback eine API, mit der die verbleibende Zeit möglichst effizient genutzt werden kann.

Schauen wir uns das etwas genauer an, um zu sehen, wie wir es nutzen können.

Nach requestIdleCallback suchen

requestIdleCallback hat zwar noch ganz am Anfang, daher sollten Sie vor der Verwendung prüfen, ob sie verfügbar ist:

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

Sie können auch das Verhalten verschieben, wofür ein Fallback auf setTimeout erforderlich ist:

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);
    }

Die Verwendung von setTimeout ist nicht ideal, da sie im Gegensatz zu requestIdleCallback nichts über die Leerlaufzeit weiß. Da Sie die Funktion jedoch direkt aufrufen würden, wenn requestIdleCallback nicht verfügbar wäre, ist es nicht schlimmer, auf diese Weise zu arbeiten. Wenn requestIdleCallback verfügbar ist, werden deine Anrufe lautlos weitergeleitet. Das ist super.

Für den Moment nehmen wir jedoch einmal an, dass sie existiert.

requestIdleCallback verwenden

Das Aufrufen von requestIdleCallback ist dem requestAnimationFrame sehr ähnlich, da als erster Parameter eine Callback-Funktion verwendet wird:

requestIdleCallback(myNonEssentialWork);

Wenn myNonEssentialWork aufgerufen wird, wird ein deadline-Objekt mit einer Funktion aufgerufen, die eine Zahl zurückgibt, die angibt, wie viel Zeit für Ihre Arbeit verbleibt:

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

Die Funktion timeRemaining kann aufgerufen werden, um den neuesten Wert zu erhalten. Wenn timeRemaining() null zurückgibt, können Sie eine weitere requestIdleCallback planen, wenn Sie noch mehr zu tun haben:

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

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

So wird garantiert, dass Ihre Funktion

Was tun Sie, wenn viel los ist? Sie befürchten, dass Ihr Callback eventuell nie aufgerufen wird. Obwohl requestIdleCallback requestAnimationFrame ähnelt, unterscheidet es sich auch darin, dass es einen optionalen zweiten Parameter benötigt: ein Optionsobjekt mit eine Zeitüberschreitung-Eigenschaft. Wenn dieses Zeitlimit festgelegt wird, gibt der Browser eine Zeit in Millisekunden, innerhalb derer der Callback ausgeführt werden muss:

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

Wenn Ihr Callback aufgrund des Zeitlimits ausgeführt wird, sehen Sie zwei Dinge:

  • timeRemaining() gibt null zurück.
  • Die Eigenschaft didTimeout des deadline-Objekts ist „true“.

Wenn Sie sehen, dass didTimeout „true“ ist, möchten Sie die Arbeit wahrscheinlich einfach ausführen und damit fertigstellen:

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);
}

Aufgrund der potenziellen Unterbrechung kann dieses Zeitlimit bei Ihren Nutzern zur Folge haben, dass Ihre App nicht mehr reagiert oder Verzögerungen aufweist. Seien Sie bei der Einstellung dieses Parameters vorsichtig. Wenn möglich, lassen Sie den Browser entscheiden, wann der Callback aufgerufen werden soll.

requestIdleCallback zum Senden von Analysedaten verwenden

Sehen wir uns an, wie Analytics-Daten über requestIdleCallback gesendet werden. In diesem Fall würden wir ein Ereignis erfassen, wie z. B. das Tippen auf ein Navigationsmenü. Da sie normalerweise auf dem Bildschirm animiert werden, sollte dieses Ereignis nicht sofort an Google Analytics gesendet werden. Wir erstellen eine Reihe von Ereignissen, die gesendet und später gesendet werden sollen:

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();
}

Jetzt müssen wir requestIdleCallback verwenden, um ausstehende Ereignisse zu verarbeiten:

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();
    }
}

Hier sehen Sie, dass ich ein Zeitlimit von 2 Sekunden festgelegt habe. Dieser Wert hängt jedoch von Ihrer Anwendung ab. Bei Analysedaten ist es sinnvoll, dass eine Zeitüberschreitung verwendet wird, um sicherzustellen, dass die Daten in einem angemessenen Zeitrahmen und nicht nur zu einem späteren Zeitpunkt gemeldet werden.

Schließlich müssen wir die Funktion schreiben, die requestIdleCallback ausführen soll.

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();
}

Für dieses Beispiel ging ich davon aus, dass die Analysedaten sofort gesendet werden sollten, wenn es requestIdleCallback nicht gäbe. In einer Produktionsanwendung ist es jedoch wahrscheinlich besser, den Sendevorgang mit einer Zeitüberschreitung zu verzögern, um sicherzustellen, dass er nicht mit Interaktionen in Konflikt steht und Verzögerungen verursacht.

„requestIdleCallback“ zum Vornehmen von DOM-Änderungen verwenden

Eine weitere Situation, in der requestIdleCallback die Leistung enorm verbessern kann, sind nicht unbedingt erforderliche DOM-Änderungen, etwa das Hinzufügen von Elementen am Ende einer ständig wachsenden Liste mit Lazy-Loading. Sehen wir uns an, wie requestIdleCallback in einen typischen Frame passt.

Ein typischer Frame.

Es kann also sein, dass der Browser zu ausgelastet ist, um Callbacks in einem bestimmten Frame auszuführen. Daher sollten Sie am Ende eines Frames keine kostenlose Zeit für weitere Aufgaben haben. Das unterscheidet sie von etwas wie setImmediate, das pro Frame ausgeführt wird.

Wenn der Callback am Ende des Frames ausgelöst wird, wird der Callback nach dem Commit des aktuellen Frames ausgeführt. Das bedeutet, dass Stiländerungen angewendet und vor allem das Layout berechnet wurden. Wenn wir DOM-Änderungen innerhalb des inaktiven Callbacks vornehmen, werden diese Layoutberechnungen ungültig. Bei Layout-Lesevorgängen im nächsten Frame, z.B. getBoundingClientRect oder clientWidth, muss der Browser ein erzwungenes synchrones Layout ausführen, was einen potenziellen Leistungsengpass verursachen kann.

Ein weiterer Grund, warum keine DOM-Änderungen im inaktiven Callback ausgelöst werden, ist, dass die zeitliche Auswirkung einer Änderung des DOMs unvorhersehbar ist, sodass wir die vom Browser vorgegebene Frist leicht überschreiten können.

Es hat sich bewährt, DOM-Änderungen nur innerhalb eines requestAnimationFrame-Callbacks vorzunehmen, da dieser vom Browser unter Berücksichtigung dieser Art von Arbeit geplant wird. Das bedeutet, dass unser Code ein Dokumentfragment verwenden muss, das dann an den nächsten requestAnimationFrame-Callback angehängt werden kann. Wenn Sie eine VDOM-Bibliothek verwenden, nehmen Sie Änderungen mit requestIdleCallback vor. Die DOM-Patches werden jedoch im nächsten requestAnimationFrame-Callback und nicht im inaktiven Callback angewandt.

Sehen wir uns dazu den Code an:

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();
}

Hier erstelle ich das Element und verwende die Eigenschaft textContent, um es auszufüllen, aber die Chancen stehen gut, dass der Code zur Elementerstellung aufwendiger wäre. Nachdem das Element scheduleVisualUpdateIfNeeded erstellt wurde, wird ein einzelner requestAnimationFrame-Callback eingerichtet, der wiederum das Dokumentfragment an den Textkörper anhängt:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

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

Jetzt wird es viel weniger Verzögerungen beim Anhängen von Elementen an das DOM geben. Sehr gut!

Häufig gestellte Fragen

  • Gibt es einen Polyfill? Leider nicht, aber es gibt einen Shim, wenn Sie eine transparente Weiterleitung zu setTimeout wünschen. Diese API schließt eine sehr reale Lücke in der Webplattform. Es ist schwierig, auf mangelnde Aktivität zu schließen, aber es gibt keine JavaScript-APIs, um die kostenlose Zeit am Ende des Frames zu bestimmen. Daher müssen Sie am besten eine Vermutung anstellen. APIs wie setTimeout, setInterval oder setImmediate können zum Planen von Aufgaben verwendet werden. Sie sind jedoch nicht zeitlich festgelegt, um Nutzerinteraktionen so zu vermeiden wie requestIdleCallback.
  • Was passiert, wenn ich die Frist überschreite? Wenn timeRemaining() null zurückgibt, Sie sich aber für eine längere Ausführung entscheiden, müssen Sie nicht befürchten, dass der Browser Ihre Arbeit anhält. Der Browser gibt Ihnen jedoch die Frist, die Sie testen müssen, um ein reibungsloses Erlebnis für Ihre Nutzer zu gewährleisten. Sie sollten sich also immer an diese Frist halten, sofern es keinen sehr guten Grund dafür gibt.
  • Gibt es einen Maximalwert, den timeRemaining() zurückgibt? Ja, aktuell sind es 50 ms. Wenn eine Anwendung responsiv gehalten werden soll, sollten alle Reaktionen auf Nutzerinteraktionen unter 100 ms bleiben. Wenn der Nutzer mit dem 50-ms-Fenster interagiert, sollte er in den meisten Fällen zulassen, dass der inaktive Callback abgeschlossen und der Browser auf die Interaktionen des Nutzers reagiert. Möglicherweise werden mehrere inaktive Callbacks nacheinander geplant, wenn der Browser feststellt, dass genügend Zeit für die Ausführung vorhanden ist.
  • Gibt es etwas, das ich bei einem requestIdleCallback nicht machen sollte? Idealerweise sollten Sie Ihre Arbeit in kleinen Blöcken (Mikroaufgaben) mit relativ vorhersehbaren Merkmalen ausführen. Beispielsweise führt das Ändern des DOM zu unvorhersehbaren Ausführungszeiten, da dadurch Stilberechnungen, Layout, Painting und Compositing ausgelöst werden. Daher sollten Sie DOM-Änderungen in einem requestAnimationFrame-Callback nur wie oben empfohlen vornehmen. Außerdem sollten Sie Promise-Objekte auflösen oder ablehnen, da die Callbacks sofort nach dem Ende des inaktiven Callbacks ausgeführt werden, auch wenn keine Zeit mehr übrig ist.
  • Erhalte ich am Ende eines Frames immer ein requestIdleCallback? Nein, nicht immer. Der Browser plant den Rückruf immer dann, wenn am Ende eines Frames kostenlose Zeit verfügbar ist, oder für Zeiträume, in denen der Nutzer inaktiv ist. Sie sollten nicht erwarten, dass der Callback pro Frame aufgerufen wird, und wenn er innerhalb eines bestimmten Zeitrahmens ausgeführt werden soll, sollten Sie das Zeitlimit nutzen.
  • Kann ich mehrere requestIdleCallback-Callbacks haben? Ja, das ist möglich, weil es mehrere requestAnimationFrame-Callbacks geben kann. Wenn Ihr erster Callback die verbleibende Zeit während des Callbacks verbraucht, bleibt keine Zeit mehr für andere Rückrufe. Die anderen Callbacks müssen dann warten, bis der Browser das nächste Mal inaktiv ist, bevor sie ausgeführt werden können. Je nachdem, welche Arbeit Sie erledigen möchten, ist es möglicherweise besser, einen einzelnen inaktiven Callback zu haben und die Arbeit dort aufzuteilen. Alternativ können Sie das Zeitlimit nutzen, um dafür zu sorgen, dass Rückrufe keine Zeit verlieren.
  • Was passiert, wenn ich einen neuen inaktiven Callback innerhalb eines anderen festlege? Der neue inaktive Callback wird so bald wie möglich ausgeführt, beginnend mit dem nächsten und nicht mit dem aktuellen.

Leerlauf!

Mit requestIdleCallback können Sie dafür sorgen, dass Ihr Code ausgeführt werden kann, ohne den Nutzer zu stören. Es ist einfach zu bedienen und sehr flexibel. Wir stehen aber noch am Anfang und mit den Spezifikationen noch nicht ganz zufrieden. Deshalb freuen wir uns über jedes Feedback.

Probiert es in Chrome Canary aus, testet den Dienst für eure Projekte und teilt uns mit, wie ihr weiter seht.