requestIdleCallback verwenden

Paul Lewis

Viele Websites und Apps haben viele Scripts, die ausgeführt werden müssen. Ihr JavaScript muss oft so schnell wie möglich ausgeführt werden, aber gleichzeitig soll es den Nutzern nicht im Weg stehen. Wenn Sie Analysedaten senden, während der Nutzer auf der Seite scrollt, oder dem DOM Elemente hinzufügen, während er auf die Schaltfläche tippt, reagiert Ihre Webanwendung möglicherweise nicht mehr, was zu einer schlechten Nutzererfahrung führt.

Mit requestIdleCallback nicht notwendige Aufgaben planen

Die gute Nachricht ist, dass es jetzt eine API gibt, die Ihnen dabei helfen kann: requestIdleCallback. So wie wir mit requestAnimationFrame Animationen richtig planen und die Chancen maximieren konnten, 60 fps zu erreichen, wird mit requestIdleCallback die Arbeit geplant, wenn am Ende eines Frames Zeit kostenlos ist oder der Nutzer inaktiv ist. So haben Sie die Möglichkeit, Ihre Arbeit zu erledigen, ohne den Nutzern im Weg zu stehen. Die Funktion ist ab Chrome 47 verfügbar. Sie können sie also schon heute mit Chrome Canary ausprobieren. Es handelt sich um eine experimentelle Funktion und die Spezifikation befindet sich noch in der Entwicklungsphase. Daher kann sich die Funktion in Zukunft noch ändern.

Warum sollte ich requestIdleCallback verwenden?

Es ist sehr schwierig, nicht notwendige Aufgaben selbst zu planen. Es ist unmöglich, genau herauszufinden, wie viel Framezeit noch übrig ist, da nach der Ausführung von requestAnimationFrame-Callbacks Stilberechnungen, Layout, Malen und andere interne Browserfunktionen ausgeführt werden müssen. Eine selbst entwickelte Lösung kann diese Probleme nicht berücksichtigen. Um sicherzugehen, dass ein Nutzer nicht auf irgendeine Weise interagiert, müssen Sie auch Listener an jede Art von Interaktionsereignis (scroll, touch, click) anhängen, auch wenn Sie sie für die Funktionalität nicht benötigen, nur damit Sie absolut sicher sein können, dass der Nutzer nicht interagiert. Der Browser hingegen weiß genau, wie viel Zeit am Ende des Frames zur Verfügung steht und ob der Nutzer interagiert. Mit requestIdleCallback erhalten wir also eine API, mit der wir die verbleibende Zeit möglichst effizient nutzen können.

Sehen wir uns das genauer an und überlegen, wie wir es nutzen können.

Prüfen, ob requestIdleCallback vorhanden ist

requestIdleCallback befindet sich noch in der Entwicklungsphase. Bevor Sie es verwenden, sollten Sie prüfen, ob es verfügbar ist:

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

Sie können das Verhalten auch anpassen. Dazu müssen Sie auf setTimeout zurückgreifen:

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 nicht wie requestIdleCallback die Inaktivitätszeit kennt. Da Sie Ihre Funktion aber direkt aufrufen würden, wenn requestIdleCallback nicht verfügbar wäre, ist ein Shimming auf diese Weise nicht schlechter. Mit dem Shim werden Ihre Anrufe bei Verfügbarkeit von requestIdleCallback automatisch weitergeleitet.

Nehmen wir vorerst an, dass es eine solche Funktion gibt.

requestIdleCallback verwenden

Der Aufruf von requestIdleCallback ähnelt dem von requestAnimationFrame, da auch hier eine Callback-Funktion als erster Parameter verwendet wird:

requestIdleCallback(myNonEssentialWork);

Wenn myNonEssentialWork aufgerufen wird, wird ihm ein deadline-Objekt übergeben, das eine Funktion enthält, die eine Zahl zurückgibt, die angibt, wie viel Zeit für Ihre Arbeit noch bleibt:

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

Die Funktion timeRemaining kann aufgerufen werden, um den aktuellen Wert abzurufen. Wenn timeRemaining() den Wert 0 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);
}

Sicherstellen, dass Ihre Funktion aufgerufen wird

Was machen Sie, wenn es sehr viel zu tun gibt? Sie haben möglicherweise Bedenken, dass Sie nie zurückgerufen werden. requestIdleCallback ähnelt zwar requestAnimationFrame, unterscheidet sich aber dadurch, dass es einen optionalen zweiten Parameter hat: ein Optionsobjekt mit der Eigenschaft „timeout“. Wenn dieses Zeitlimit festgelegt ist, gibt es dem Browser eine Zeit in Millisekunden, bis zu der er den Rückruf ausführen muss:

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

Wenn Ihr Rückruf aufgrund einer Zeitüberschreitung ausgeführt wird, fallen Ihnen zwei Dinge auf:

  • timeRemaining() gibt dann null zurück.
  • Das Attribut didTimeout des Objekts deadline hat den Wert „true“.

Wenn didTimeout „wahr“ ist, möchten Sie die Arbeit wahrscheinlich einfach ausführen und fertig sein:

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

Achten Sie bei der Festlegung dieses Parameters auf Ihre Nutzer, da die Ausführung der Arbeit dazu führen kann, dass Ihre App nicht mehr reagiert oder ruckelt. Lassen Sie nach Möglichkeit den Browser entscheiden, wann der Rückruf aufgerufen werden soll.

requestIdleCallback zum Senden von Analysedaten verwenden

Sehen wir uns an, wie Sie mit requestIdleCallback Analysedaten senden. In diesem Fall möchten wir wahrscheinlich ein Ereignis erfassen, z. B. das Tippen auf ein Navigationsmenü. Da sie jedoch normalerweise animiert auf dem Bildschirm erscheinen, sollten wir dieses Ereignis nicht sofort an Google Analytics senden. Wir erstellen eine Reihe von Ereignissen, die gesendet werden sollen, und bitten darum, dass sie zu einem bestimmten Zeitpunkt gesendet werden:

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 alle ausstehenden 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 habe ich eine Zeitüberschreitung von 2 Sekunden festgelegt. Dieser Wert hängt jedoch von Ihrer Anwendung ab. Bei Analysedaten ist es sinnvoll, eine Zeitüberschreitung zu verwenden, damit Daten innerhalb eines angemessenen Zeitraums und nicht erst irgendwann in der Zukunft erfasst werden.

Schließlich müssen wir die Funktion schreiben, die von requestIdleCallback ausgeführt wird.

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

In diesem Beispiel wurde davon ausgegangen, dass die Analysedaten sofort gesendet werden sollen, wenn requestIdleCallback nicht vorhanden ist. In einer Produktionsanwendung ist es jedoch wahrscheinlich besser, das Senden mit einer Zeitüberschreitung zu verzögern, damit es nicht mit Interaktionen in Konflikt gerät und zu Rucklern führt.

DOM-Änderungen mit requestIdleCallback vornehmen

Eine weitere Situation, in der requestIdleCallback die Leistung wirklich verbessern kann, ist, wenn Sie nicht unbedingt notwendige DOM-Änderungen vornehmen müssen, z. B. Elemente am Ende einer ständig wachsenden, lazy-geladenen Liste hinzufügen. Sehen wir uns an, wie requestIdleCallback in einen typischen Frame passt.

Ein typischer Frame.

Es ist möglich, dass der Browser zu beschäftigt ist, um Callbacks in einem bestimmten Frame auszuführen. Daher sollten Sie nicht davon ausgehen, dass am Ende eines Frames Zeit für weitere Aufgaben zur Verfügung steht. Das unterscheidet es von setImmediate, das pro Frame ausgeführt wird.

Wenn der Rückruf am Ende des Frames ausgelöst wird, wird er so geplant, dass er nach dem Commit des aktuellen Frames ausgeführt wird. Das bedeutet, dass Stiländerungen angewendet und das Layout berechnet wurde. Wenn wir im Inaktivitäts-Callback DOM-Änderungen vornehmen, werden diese Layoutberechnungen ungültig. Wenn im nächsten Frame Layoutlesungen vorhanden sind, z. B. getBoundingClientRect oder clientWidth, muss der Browser ein erzwungenes synchrones Layout ausführen. Dies kann zu Leistungsengpässen führen.

Ein weiterer Grund, DOM-Änderungen nicht im Callback für Inaktivität auszulösen, ist, dass die Zeit, die für die Änderung des DOM benötigt wird, nicht vorhersehbar ist. Daher könnten wir leicht den vom Browser angegebenen Termin verpassen.

Es wird empfohlen, DOM-Änderungen nur innerhalb eines requestAnimationFrame-Callbacks vorzunehmen, da dieser vom Browser für diese Art von Arbeit geplant wird. Das bedeutet, dass unser Code ein Dokumentfragment verwenden muss, das dann im nächsten requestAnimationFrame-Callback angehängt werden kann. Wenn du eine VDOM-Bibliothek verwendest, kannst du Änderungen mit requestIdleCallback vornehmen, die DOM-Patches werden aber im nächsten requestAnimationFrame-Callback angewendet, nicht im IDLE-Callback.

Sehen wir uns 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 textContent-Eigenschaft, um es zu füllen. Wahrscheinlich ist Ihr Code zum Erstellen von Elementen jedoch komplexer. Nach dem Erstellen des Elements wird scheduleVisualUpdateIfNeeded aufgerufen, wodurch ein einzelner requestAnimationFrame-Callback eingerichtet wird, der das Dokumentenfragment wiederum 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;
}

Wenn alles gut geht, sollten wir jetzt viel weniger Ruckler beim Anhängen von Elementen an das DOM sehen. Sehr gut!

FAQ

  • Gibt es eine Polyfill-Lösung? Leider nicht, aber es gibt einen Shim, wenn Sie eine transparente Weiterleitung zu setTimeout wünschen. Diese API wurde entwickelt, um eine sehr reale Lücke in der Webplattform zu schließen. Es ist schwierig, einen Mangel an Aktivität zu erkennen. Es gibt jedoch keine JavaScript-APIs, mit denen sich die kostenlose Zeit am Ende des Frames bestimmen lässt. Sie müssen also im besten Fall Vermutungen anstellen. Mit APIs wie setTimeout, setInterval oder setImmediate können Aufgaben geplant werden, sie sind jedoch nicht so zeitlich abgestimmt, dass Nutzerinteraktionen vermieden werden, wie es bei requestIdleCallback der Fall ist.
  • Was passiert, wenn ich die Frist verpasse? Wenn timeRemaining() den Wert 0 zurückgibt, Sie aber die Ausführung länger laufen lassen möchten, können Sie dies tun, ohne dass der Browser Ihre Arbeit unterbricht. Der Browser gibt Ihnen jedoch einen Termin, um für eine reibungslose Nutzung zu sorgen. Außer bei triftigen Gründen sollten Sie diesen Termin daher immer einhalten.
  • Gibt es einen maximalen Wert, der von timeRemaining() zurückgegeben wird? Ja, derzeit sind es 50 ms. Wenn Sie eine responsive Anwendung entwickeln möchten, sollten alle Antworten auf Nutzerinteraktionen unter 100 ms liegen. Sollte der Nutzer interagieren, sollte das 50-ms-Fenster in den meisten Fällen ausreichen, damit der Inaktivitäts-Callback abgeschlossen werden und der Browser auf die Interaktionen des Nutzers reagieren kann. Es kann vorkommen, dass mehrere Inaktivitäts-Callbacks hintereinander geplant werden, wenn der Browser feststellt, dass genügend Zeit für die Ausführung vorhanden ist.
  • Gibt es Aufgaben, die ich in einem requestIdleCallback nicht ausführen sollte? Idealerweise sollten Sie Ihre Arbeit in kleine Teile (Mikroaufgaben) unterteilen, die relativ vorhersehbare Eigenschaften haben. Insbesondere das Ändern des DOM führt zu unvorhersehbaren Ausführungszeiten, da dadurch Stilberechnungen, Layout, Malen und Compositing ausgelöst werden. Daher sollten Sie DOM-Änderungen nur in einem requestAnimationFrame-Callback vornehmen, wie oben vorgeschlagen. Außerdem ist Vorsicht geboten, wenn Versprechen erfüllt (oder abgelehnt) werden, da die Callbacks sofort nach Abschluss des Inaktivitäts-Callbacks ausgeführt werden, auch wenn keine Zeit mehr verbleibt.
  • Erhalte ich immer ein requestIdleCallback am Ende eines Frames? Nein, nicht immer. Der Browser plant den Callback immer dann, wenn am Ende eines Frames oder in Zeiträumen, in denen der Nutzer inaktiv ist, kostenlose Zeit vorhanden ist. Sie sollten nicht davon ausgehen, dass der Rückruf pro Frame aufgerufen wird. Wenn er innerhalb eines bestimmten Zeitraums ausgeführt werden soll, sollten Sie die Zeitüberschreitung verwenden.
  • Kann ich mehrere requestIdleCallback-Callbacks haben? Ja, genau wie bei mehreren requestAnimationFrame-Callbacks. Wenn Sie jedoch die verbleibende Zeit für den ersten Rückruf aufbrauchen, bleibt für weitere Rückrufe keine Zeit mehr übrig. Die anderen Rückrufe müssen dann warten, bis der Browser das nächste Mal inaktiv ist, bevor sie ausgeführt werden können. Je nachdem, was Sie erledigen möchten, ist es möglicherweise besser, einen einzelnen Callback für den Inaktivitätsstatus zu haben und die Arbeit dort aufzuteilen. Alternativ können Sie die Zeitüberschreitung verwenden, um zu verhindern, dass Callbacks zu lange warten müssen.
  • Was passiert, wenn ich einen neuen Inaktivitäts-Callback in einem anderen festlege? Der neue Inaktivitäts-Callback wird so bald wie möglich geplant, beginnend mit dem nächsten Frame (nicht dem aktuellen).

Inaktiv.

requestIdleCallback ist eine hervorragende Möglichkeit, Ihren Code auszuführen, ohne den Nutzer zu stören. Sie ist einfach zu bedienen und sehr flexibel. Wir stehen jedoch noch ganz am Anfang und die Spezifikation ist noch nicht vollständig festgelegt. Daher freuen wir uns über jedes Feedback.

Probieren Sie es in Chrome Canary aus und teilen Sie uns Ihre Erfahrungen mit.