requestIdleCallback verwenden

Paul Lewis

Viele Websites und Apps haben viele Scripts, die ausgeführt werden müssen. Ihr JavaScript-Code muss oft so schnell wie möglich ausgeführt werden, gleichzeitig aber nicht möchten, dass er die Nutzer behindert. 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 Chance maximieren, 60 fps zu erreichen, plant requestIdleCallback die Arbeit dann, wenn am Ende eines Frames kostenlose Zeit verfügbar ist oder wenn 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, unwichtige Arbeiten selbst zu planen. Sie können nicht genau herausfinden, wie viel Frame Time noch übrig ist, da nach der Ausführung von requestAnimationFrame-Callbacks Stilberechnungen, Layout, Paint und andere Browser-Interna ausgeführt werden müssen. Eine selbst entwickelte Lösung kann diese Probleme nicht berücksichtigen. Damit Nutzer auf irgendeine Weise nicht interagieren, müssen Sie jedem Interaktionsereignis (scroll, touch, click) Listener hinzufügen, selbst wenn Sie diese für die Funktionalität nicht benötigen. Nur so können Sie sicher sein, dass der Nutzer nicht interagiert. Der Browser hingegen weiß genau, wie viel Zeit am Ende des Frames verfügbar ist und ob der Nutzer interagiert. Durch requestIdleCallback erhalten wir eine API, die es uns ermöglicht, freie Zeit möglichst effizient zu nutzen.

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. Wenn requestIdleCallback mit dem Shim verfügbar ist, werden Ihre Anrufe automatisch weitergeleitet.

Gehen wir vorerst jedoch davon aus, dass es eine solche Funktion gibt.

requestIdleCallback verwenden

Der Aufruf von requestIdleCallback ähnelt requestAnimationFrame insofern, als als erster Parameter eine Callback-Funktion 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() Null zurückgibt, können Sie einen weiteren requestIdleCallback planen, wenn Sie noch mehr zu erledigen 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 tun Sie, wenn es sehr viel zu tun gibt? Sie haben vielleicht Bedenken, dass Sie nie zurückgerufen werden. Obwohl requestIdleCallback requestAnimationFrame ähnelt, unterscheidet es sich auch dadurch, dass ein optionaler zweiter Parameter erforderlich ist: ein Optionsobjekt mit einem Zeitlimit. Dieses Zeitlimit, sofern festgelegt, gibt dem Browser eine Zeit in Millisekunden an, in der er den Callback 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.
  • Die Eigenschaft didTimeout des Objekts deadline ist „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, wenn requestIdleCallback nicht vorhanden ist. In einer Produktionsanwendung ist es jedoch wahrscheinlich besser, den Sendevorgang mit einer Zeitüberschreitung zu verzögern, damit es nicht zu Konflikten mit Interaktionen und zu Verzögerungen kommt.

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 irgendeine Art von Layout-Lesevorgängen stattfindet, z. B. getBoundingClientRect, clientWidth usw., muss der Browser ein erzwungenes synchrones Layout ausführen, was zu einem Leistungsengpass führt.

Ein weiterer Grund, DOM-Änderungen nicht im Inaktivitäts-Callback auszulösen, ist, dass die Zeit, die für die Änderung des DOM benötigt wird, nicht vorhersehbar ist. Daher könnten wir leicht die vom Browser vorgegebene Frist 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 Sie eine VDOM-Bibliothek verwenden, verwenden Sie requestIdleCallback, um Änderungen vorzunehmen, aber wenden die DOM-Patches im nächsten requestAnimationFrame-Callback an, 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 Eigenschaft textContent, um es zu füllen. Es ist aber wahrscheinlich, dass Ihr Code zur Elementerstellung aufwendiger wäre. 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 werden jedoch nicht so getaktet, dass Nutzerinteraktionen vermieden werden, wie es bei requestIdleCallback der Fall ist.
  • Was passiert, wenn ich die Frist überschreite? 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 etwas, das ich in „requestIdleCallback“ nicht erledigen 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 solltest du darauf achten, Versprechen nicht zu lösen (oder abzulehnen), da die Callbacks sofort nach Abschluss des Inaktivitäts-Callbacks ausgeführt werden, auch wenn keine Zeit mehr übrig ist.
  • 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, Zeit 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. Denken Sie jedoch daran, dass, wenn Ihr erster Callback die verbleibende Zeit während seines Callbacks verbraucht, keine Zeit mehr für andere Callbacks bleibt. 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 inaktive Callback wird so bald wie möglich ausgeführt, und zwar ab dem nächsten Frame (und nicht mit 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.