Korzystanie z requestIdleCallback

Wiele stron i aplikacji zawiera wiele skryptów do wykonania. JavaScript często trzeba uruchamiać jak najszybciej, ale jednocześnie nie chcesz, aby przeszkadzał użytkownikowi. Jeśli wysyłasz dane analityczne, gdy użytkownik przewija stronę, lub dołączasz elementy do DOM, gdy klikają przycisk, Twoja aplikacja internetowa może przestać reagować, co pogarsza wrażenia użytkowników.

Planowanie mniej ważnych zadań za pomocą requestIdleCallback.

Dobra wiadomość jest taka, że dostępny jest teraz interfejs API, który może Ci w tym pomóc: requestIdleCallback. Tak samo jak dzięki wdrożeniu requestAnimationFrame mogliśmy prawidłowo zaplanować animacje i zmaksymalizować szanse na uzyskanie 60 klatek na sekundę, requestIdleCallback planuje działanie w czasie wolnym po zakończeniu klatki lub gdy użytkownik jest nieaktywny. Oznacza to, że można wykonać swoją pracę bez ingerencji użytkownika w proces. Jest dostępna od Chrome 47, więc możesz wypróbować ją już dziś, używając Chrome Canary. To funkcja eksperymentalna, a jej specyfikacja wciąż się zmienia, więc w przyszłości może się to zmienić.

Dlaczego należy używać requestIdleCallback?

Samodzielne planowanie prac mniej ważnych jest bardzo trudne. Nie da się dokładnie określić, ile czasu pozostało do renderowania klatki, ponieważ po wykonaniu wywołań zwrotnych requestAnimationFrame konieczne są obliczenia stylu, układ, renderowanie i inne elementy przeglądarki. Rozwiązanie, które można wdrożyć w domu, nie jest w stanie ich uwzględniać. Aby mieć pewność, że użytkownik nie wchodzi w jakąś interakcję, musisz też dołączyć detektory do każdego rodzaju zdarzenia interakcji (scroll, touch, click), nawet jeśli nie są one niezbędne do obsługi funkcji, tylko tak, by mieć absolutną pewność, że użytkownik nie wchodzi w interakcję. Przeglądarka wie natomiast dokładnie, ile czasu zostało wykorzystane na końcu klatki i czy użytkownik wchodzi z nią w interakcję, więc dzięki requestIdleCallback otrzymujemy interfejs API, który pozwala nam efektywnie wykorzystać wolny czas.

Przyjrzyjmy się mu nieco bardziej szczegółowo i zastanówmy się, jak możemy go wykorzystać.

Sprawdzam identyfikator żądania IleCallback

Usługa requestIdleCallback jest na wczesnym etapie rozwoju, więc zanim z niej skorzystasz, sprawdź, czy jest gotowa:

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

Możesz też ukryć jego działanie, co wymaga powrotu do 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);
    }

Korzystanie z funkcji setTimeout nie jest przydatne, ponieważ nie wie o czasie bezczynności tak jak w przypadku funkcji requestIdleCallback, ale gdyby requestIdleCallback byłby niedostępny, nie trzeba w ten sposób korzystać z podświetlania. Jeśli podkładka jest dostępna, połączenia requestIdleCallback będą przekierowywane po cichu – to świetnie.

Na razie załóżmy jednak, że ona istnieje.

Korzystanie z requestIdleCallback

Wywołanie requestIdleCallback jest bardzo podobne do requestAnimationFrame, ponieważ jako pierwszy parametr wykorzystuje funkcję wywołania zwrotnego:

requestIdleCallback(myNonEssentialWork);

Po wywołaniu funkcji myNonEssentialWork otrzyma on obiekt deadline zawierający funkcję zwracającą liczbę wskazującą, ile czasu pozostało na pracę:

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

Funkcję timeRemaining można wywołać, aby uzyskać najnowszą wartość. Gdy timeRemaining() zwróci 0, możesz zaplanować kolejne requestIdleCallback, jeśli masz jeszcze więcej do zrobienia:

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

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

Zapewnienie funkcji nazywa się

Co robisz, gdy masz dużo na głowie? Obawiasz się, że oddzwanianie nigdy nie zostanie nawiązane. Chociaż requestIdleCallback przypomina requestAnimationFrame, różni się też tym, że wymaga użycia dodatkowego drugiego parametru: obiektu options z właściwością limit czasu. Ustawiony limit czasu oczekiwania określa w milisekundach czas, w którym przeglądarka musi wykonać wywołanie zwrotne:

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

Jeśli wywołanie zwrotne zostanie wykonane z powodu przekroczenia limitu czasu oczekiwania, zobaczysz dwie rzeczy:

  • timeRemaining() zwróci wartość 0.
  • Właściwość didTimeout obiektu deadline ma wartość prawda.

Jeśli zobaczysz, że didTimeout ma wartość prawda, prawdopodobnie chcesz po prostu uruchomić wybraną pracę:

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

Ze względu na potencjalne zakłócenia działania ten czas oczekiwania może spowodować u użytkowników (praca może spowodować, że aplikacja przestanie odpowiadać lub działać nieprawidłowo), więc zachowaj ostrożność podczas ustawiania tego parametru. Jeśli jest to możliwe, pozwól przeglądarce zdecydować, kiedy wywołać wywołanie zwrotne.

Korzystanie z requestIdleCallback do wysyłania danych analitycznych

Przyjrzyjmy się, jak requestIdleCallback wysyła dane analityczne. W tym przypadku chcemy śledzić zdarzenie, takie jak kliknięcie menu nawigacyjnego. Ponieważ jednak zwykle są one animowane na ekranie, chcemy uniknąć natychmiastowego wysyłania tego zdarzenia do Google Analytics. Utworzymy tablicę zdarzeń do wysłania i żądania, aby zostały wysłane w przyszłości:

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

Teraz musimy użyć adresu requestIdleCallback do przetworzenia oczekujących zdarzeń:

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

Jak widać, limit czasu został ustawiony na 2 sekundy, ale zależy to od aplikacji. W przypadku danych analitycznych sensowne jest ustawienie limitu czasu, aby mieć pewność, że dane są raportowane w rozsądnym czasie, a nie tylko w jakimś momencie w przyszłości.

Na koniec musimy napisać funkcję, którą requestIdleCallback wykona.

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

Na tym przykładzie zakładałem, że skoro requestIdleCallback nie istnieje, dane analityczne powinny zostać wysłane natychmiast. Jednak w aplikacji produkcyjnej lepiej jest opóźnić wysyłanie wiadomości o określony czas oczekiwania, aby uniknąć konfliktu z interakcjami i zacinania się.

Wprowadzanie zmian DOM za pomocą metody requestIdleCallback

Inną sytuacją, w której requestIdleCallback może pomóc zwiększyć skuteczność, jest konieczność wprowadzenia mniej istotnych zmian DOM, np. dodania elementów na końcu stale rosnącej, leniwie ładowanej listy. Sprawdźmy, jak requestIdleCallback wpisuje się w typowy kadr.

Typowa klatka.

Możliwe, że przeglądarka będzie zbyt zajęta, aby uruchamiać jakiekolwiek wywołania zwrotne w danej ramce, więc nie należy oczekiwać, że pod koniec klatki będzie jakikolwiek wolny czas na wykonanie innej pracy. To odróżnia go od typu setImmediate, który wyświetla reklamę na klatkę.

Jeśli wywołanie zwrotne zostanie uruchomione na końcu klatki, zostanie zaplanowane po zatwierdzeniu bieżącej klatki, co oznacza, że zmiany stylu zostaną zastosowane, a co najważniejsze – obliczony zostanie układ. Jeśli wprowadzimy zmiany DOM w bezczynnym wywołaniu zwrotnym, obliczenia układu zostaną unieważnione. Jeśli w następnej ramce zostaną zarejestrowane jakieś odczyty układu, np. getBoundingClientRect, clientWidth itp. przeglądarka musi zastosować wymuszony układ synchroniczny, co może spowodować wąskie gardło wydajności.

Innym powodem, dla którego zmiany DOM nie są wywoływane w przypadku nieaktywnego wywołania zwrotnego, jest nieprzewidywalny wpływ zmian DOM na czas, w związku z czym możemy z łatwością przekroczyć termin określony przez przeglądarkę.

Zaleca się wprowadzanie zmian DOM tylko w wywołaniu zwrotnym requestAnimationFrame, ponieważ jest to planowane przez przeglądarkę z uwzględnieniem tego rodzaju pracy. Oznacza to, że nasz kod musi używać fragmentu dokumentu, który można dołączyć w następnym wywołaniu zwrotnym requestAnimationFrame. Jeśli korzystasz z biblioteki VDOM, użyj requestIdleCallback do wprowadzenia zmian, ale zastosuj poprawki DOM przy następnym wywołaniu zwrotnym requestAnimationFrame, a nie w bezczynnym wywołaniu zwrotnym.

Mając to na uwadze, spójrzmy na kod:

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

Utworzę tutaj element i użyję właściwości textContent, aby go wypełnić, ale możliwe, że kod tworzenia elementu będzie bardziej zaangażowany. Po utworzeniu elementu scheduleVisualUpdateIfNeeded zostanie wywołany jedno wywołanie zwrotne requestAnimationFrame, które z kolei spowoduje dołączenie fragmentu dokumentu do treści:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

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

Teraz przy dołączaniu elementów do DOM nie zauważysz już zbyt wielu zacięć. Znakomity

Najczęstsze pytania

  • Czy występuje kod polyfill? Niestety nie. Jeśli chcesz ustawić przezroczyste przekierowanie do setTimeout, jest dostępna podkładka. Ten interfejs API istnieje, ponieważ wypełnia prawdziwą lukę na platformie internetowej. Trudno wywnioskować brak aktywności, ale nie ma żadnych interfejsów API JavaScript służących do określania ilości wolnego czasu na końcu ramki, więc najlepiej zgadywać. Do planowania pracy można używać interfejsów API takich jak setTimeout, setInterval lub setImmediate, ale ich dostępność nie jest ograniczona czasowo, aby uniknąć interakcji użytkownika w taki sposób, jak requestIdleCallback.
  • Co się stanie, jeśli przekroczę termin? Jeśli timeRemaining() zwróci 0, ale zdecydujesz się biegać dłużej, możesz to zrobić bez obaw, że przeglądarka przerwie Twoją pracę. Pamiętaj jednak o terminie, w którym należy zadbać o wygodę użytkowników. Dlatego pamiętaj, by zawsze dotrzymać wyznaczonego terminu, jeśli nie ma ważnego powodu.
  • Czy istnieje maksymalna wartość, którą timeRemaining() zwróci? Tak, obecnie jest to 50 ms. Jeśli chcesz utrzymać elastyczną aplikację, wszystkie odpowiedzi na interakcje użytkownika powinny być krótsze niż 100 ms. Jeśli użytkownik wejdzie w interakcję z oknem 50 ms, w większości przypadków powinno ono zakończyć się bezczynne wywołanie zwrotne i umożliwić przeglądarce reagowanie na interakcje użytkownika. Możesz otrzymać wiele nieaktywnych wywołań zwrotnych zaplanowanych po kolei (jeśli przeglądarka ustali, że ma wystarczająco dużo czasu na ich uruchomienie).
  • Czy w polu requestIdleCallback nie należy wykonywać pewnych działań? Najlepiej, aby Twoja praca była wykonywana na niewielkich fragmentach (mikrozadaniach), które mają względnie przewidywalne cechy. Na przykład zmiana DOM może skutkować nieprzewidywalnym czasem wykonania, ponieważ aktywuje obliczenia stylu, układ, malowanie i komponowanie. Z tego względu zmiany DOM należy wprowadzać tylko w wywołaniu zwrotnym requestAnimationFrame w sposób pokazany powyżej. Należy też uważać na realizację (lub odrzucenie) obietnic, ponieważ wywołania zwrotne są wykonywane natychmiast po zakończeniu nieaktywnego wywołania zwrotnego, nawet jeśli nie pozostał już żaden czas.
  • Czy na końcu klatki zawsze wyświetla się requestIdleCallback? Nie, nie zawsze. Przeglądarka zaplanuje wywołanie zwrotne za każdym razem, gdy pod koniec klatki lub w okresach, w których użytkownik będzie nieaktywny. Nie należy oczekiwać, że wywołanie zwrotne będzie wywoływane za każdą ramkę, a jeśli chcesz, aby było wykonywane w określonym przedziale czasu, należy zastosować limit czasu.
  • Czy mogę mieć kilka wywołań zwrotnych requestIdleCallback? Tak, możesz, ale możesz mieć wiele wywołań zwrotnych requestAnimationFrame. Trzeba jednak pamiętać, że jeśli pierwsze wywołanie zwrotne zużywa czas pozostały na to wywołanie, nie będzie już na to więcej czasu. Pozostałe wywołania zwrotne będą musiały odczekać, aż przeglądarka przestaje być bezczynna. W zależności od pracy, którą próbujesz wykonać, lepiej użyć pojedynczego nieaktywnego wywołania zwrotnego i podzielić tam swoją pracę. Możesz też wykorzystać limit czasu, aby mieć pewność, że żadne wywołania zwrotne nie będą niepotrzebnie głodne przez długi czas.
  • Co się stanie, jeśli nowe nieaktywne wywołanie zwrotne ustawię w innym? Nowe nieaktywne wywołanie zwrotne zostanie zaplanowane tak szybko, jak to będzie możliwe, począwszy od następnej klatki (a nie od bieżącej).

Bezczynnie!

requestIdleCallback to świetny sposób na sprawdzenie, czy kod można uruchomić bez przeszkadzania użytkownikowi. Program jest prosty w obsłudze i bardzo elastyczny. To jednak wciąż dopiero początek, a specyfikacja nie jest jeszcze w pełni opracowana, więc chętnie poznamy Twoją opinię.

Wypróbuj Chrome Canary, wypróbuj swoje projekty i daj nam znać, jak Ci poszło.