Korzystanie z requestIdleCallback

Wiele stron i aplikacji zawiera wiele skryptów do wykonania. Kod JavaScript musi być często uruchamiany tak szybko, jak to możliwe, ale jednocześnie nie powinien przeszkadzać użytkownikowi. Jeśli wysyłasz dane analityczne, gdy użytkownik przewija stronę, lub dołączasz elementy do DOM, gdy użytkownik klika przycisk, Twoja aplikacja internetowa może przestać odpowiadać, co może negatywnie wpłynąć na wrażenia użytkownika.

Używanie funkcji requestIdleCallback do planowania nieistotnych zadań.

Dobra wiadomość jest taka, że istnieje 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 osiągnięcia 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 ona dostępna od wersji Chrome 47, więc możesz ją wypróbować już dziś w Chrome Canary. To funkcja eksperymentalna, a jej specyfikacja wciąż się zmienia, więc w przyszłości może się to zmienić.

Dlaczego warto używać funkcji requestIdleCallback?

Samodzielne planowanie nieistotnych zadań jest bardzo trudne. Nie da się dokładnie określić, ile czasu pozostało do końca kadru, ponieważ po wykonaniu wywołań zwrotnych requestAnimationFrame występują obliczenia stylu, układ, malowanie i inne wewnętrzne operacje przeglądarki. Rozwiązanie opracowane samodzielnie nie uwzględnia żadnego z tych aspektów. Aby mieć pewność, że użytkownik nie w żaden sposób nie wchodzi w interakcję, musisz też do każdego rodzaju zdarzenia interakcji (scroll, touch, click) dołączyć słuchaczy, nawet jeśli nie są one potrzebne do działania aplikacji. Tylko w ten sposób możesz mieć pewność, że użytkownik nie wchodzi w interakcję. Z drugiej strony przeglądarka wie dokładnie, ile czasu jest dostępne na końcu ramki i czy użytkownik wchodzi w interakcję. Dzięki requestIdleCallback otrzymujemy interfejs API, który pozwala nam wykorzystać dowolny wolny czas w najbardziej efektywny sposób.

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

Sprawdzam identyfikator żądania ileCallback

Usługa requestIdleCallback jest jeszcze w fazie testów, dlatego przed jej użyciem sprawdź, czy jest dostępna:

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

Możesz też zastosować obejście, które wymaga 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. Dzięki podkładce requestIdleCallback połączenia będą przekierowywane po cichu, co jest dobrą wiadomością.

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

Używanie funkcji requestIdleCallback

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

requestIdleCallback(myNonEssentialWork);

Gdy wywołana zostanie funkcja myNonEssentialWork, otrzyma ona obiekt deadline, który zawiera funkcję zwracającą liczbę wskazującą, ile czasu pozostało do zakończenia pracy:

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

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

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

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

Zapewnienie wywołania funkcji

Co robisz, gdy jest bardzo dużo pracy? Obawiasz się, że oddzwanianie może nigdy nie zostać nawiązane. Chociaż requestIdleCallback przypomina requestAnimationFrame, różni się też tym, że obejmuje opcjonalny drugi parametr: obiekt „options” z właściwością limit czasu. Ten czas oczekiwania, jeśli jest ustawiony, określa czas w milisekundach, 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 upływu limitu czasu, zauważysz 2 rzeczy:

  • Funkcja timeRemaining() zwróci wartość 0.
  • Właściwość didTimeout obiektu deadline będzie miała wartość Prawda.

Jeśli widzisz, że didTimeout ma wartość Prawda, najprawdopodobniej chcesz po prostu uruchomić zadanie i skończyć:

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, które ten limit czasu może spowodować u użytkowników (praca może spowodować, że aplikacja przestanie odpowiadać lub będzie działać nieprawidłowo), należy zachować ostrożność przy ustawianiu tego parametru. Jeśli to możliwe, pozwól przeglądarce decydować, kiedy wywołać funkcję wywołania zwrotnego.

Używanie funkcji requestIdleCallback do wysyłania danych analitycznych

Przyjrzyjmy się, jak requestIdleCallback wysyła dane analityczne. W tym przypadku prawdopodobnie chcielibyśmy śledzić zdarzenie, np. kliknięcie menu nawigacyjnego. Ponieważ jednak zwykle są one animowane na ekranie, nie chcemy od razu wysyłać tego zdarzenia do Google Analytics. Utworzymy tablicę zdarzeń do wysłania i poprosimy o ich wysłanie w określonym momencie 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ć funkcji requestIdleCallback, aby przetworzyć oczekujące zdarzenia:

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 użycie limitu czasu, aby dane były raportowane w rozsądnym przedziale czasu, a nie tylko w jakimś momencie w przyszłości.

Na koniec musimy napisać funkcję, którą wykona 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();
}

W tym przykładzie założyłem, że jeśli element 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ę.

Używanie funkcji requestIdleCallback do wprowadzania zmian w DOM

Inną sytuacją, w której requestIdleCallback może naprawdę pomóc w zwiększeniu wydajności, jest konieczność wprowadzenia nieistotnych zmian w DOM, takich jak dodawanie elementów na końcu stale rosnącej listy wczytywanej z opóźnieniem. Zobaczmy, jak requestIdleCallback pasuje do typowego kadru.

Typowy kadr

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. Różni się to od funkcji setImmediate, która jest wykonywana na każdą klatkę.

Jeśli wywołanie zwrotne jest wywoływane na końcu ramki, zostanie zaplanowane na moment po zaakceptowaniu bieżącej ramki, co oznacza, że zmiany stylu zostaną zastosowane, a co ważne, zostanie obliczony układ. Jeśli w ramach wywołania zwrotnego w stanie bezczynności wprowadzimy zmiany w DOM, obliczenia układu zostaną unieważnione. Jeśli w następnym klatce występują jakiekolwiek odczyty układu, np.getBoundingClientRect, clientWidth itd., przeglądarka musi wykonać wymuszony synchroniczny układ, co może spowodować spowolnienie działania.

Kolejnym powodem, dla którego nie wywołujemy zmian w DOM w ramach funkcji z dodatkowym wywołaniem w stanie bezczynności, jest to, że czas potrzebny na zmianę DOM jest nieprzewidywalny, przez co łatwo możemy przekroczyć termin określony przez przeglądarkę.

Sprawdzoną metodą jest wprowadzanie zmian w DOM tylko w ramach funkcji zwracającej wywołanie requestAnimationFrame, ponieważ jest ona zaplanowana przez przeglądarkę z myślą o tym typie 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 używasz biblioteki VDOM, do wprowadzania zmian używasz funkcji requestIdleCallback, ale zastosowanie poprawek DOM następuje w następnym wywołaniu requestAnimationFrame, a nie w wywołaniu w stanie bezczynności.

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

Tutaj tworzę element i używam właściwości textContent, aby go wypełnić, ale prawdopodobnie Twój kod tworzenia elementu będzie bardziej skomplikowany. Po utworzeniu elementu wywoływana jest funkcja scheduleVisualUpdateIfNeeded, która skonfiguruje pojedynczy wywołanie zwrotne requestAnimationFrame, które z kolei doda fragment 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 jest dostępna funkcja polyfill? Niestety nie. Jeśli chcesz ustawić przezroczyste przekierowanie do setTimeout, jest dostępna podkładka. Ten interfejs API istnieje, ponieważ wypełnia bardzo realną lukę w platformie internetowej. Ustalenie braku aktywności jest trudne, ale nie ma interfejsów JavaScript API, które mogłyby określić ilość wolnego czasu na końcu ramki, więc w najlepszym razie trzeba zgadywać. Do planowania pracy można używać interfejsów API, takich jak setTimeout, setInterval czy setImmediate, ale nie są one tak zsynchronizowane, aby uniknąć interakcji z użytkownikiem w sposób, w jaki robi to interfejs requestIdleCallback.
  • Co się stanie, jeśli przekroczę termin? Jeśli timeRemaining() zwróci wartość zero, ale zdecydujesz się na dłuższe działanie, możesz to zrobić bez obawy, że przeglądarka przerwie pracę. Jednak przeglądarka podaje Ci termin, aby zapewnić użytkownikom płynne działanie, dlatego zawsze powinieneś go przestrzegać, chyba że masz bardzo dobry powód, aby tego nie robić.
  • Czy istnieje maksymalna wartość, jaką zwróci funkcja timeRemaining()? Tak, obecnie wynosi 50 ms. Aby aplikacja była responsywna, czas reakcji na interakcje użytkownika powinien wynosić poniżej 100 ms. W większości przypadków 50 ms powinno wystarczyć na wykonanie wywołania zwrotnego w trybie bezczynności i na odpowiedź przeglądarki na interakcje użytkownika. Możesz mieć zaplanowane wiele wywołań zwrotnych w trybie bezczynności (jeśli przeglądarka uzna, że jest wystarczająco dużo czasu na ich wykonanie).
  • Czy jest jakiś rodzaj pracy, której nie należy wykonywać w ramach metody requestIdleCallback? Najlepiej, aby Twoja praca była wykonywana na niewielkich fragmentach (mikrozadaniach), które mają względnie przewidywalne cechy. Na przykład zmiana DOM będzie miała nieprzewidywalny czas wykonania, ponieważ spowoduje obliczenia stylu, układu, malowania i kompozycji. Z tego powodu zmiany w DOM należy wprowadzać tylko w wywołaniu zwrotnym requestAnimationFrame, jak opisano powyżej. Inną rzeczą, na którą należy uważać, jest rozwiązywanie (lub odrzucanie) obietnic, ponieważ wywołania zwrotne będą wykonywane natychmiast po zakończeniu wywołania zwrotnego w stanie bezczynności, nawet jeśli nie pozostało już czasu.
  • Czy na końcu każdego kadru zawsze będzie widoczny element requestIdleCallback? Nie, nie zawsze. Przeglądarka zaplanowała wywołanie zwrotne na czas, gdy jest wolny czas na końcu ramki lub w okresach, gdy użytkownik jest nieaktywny. Nie należy oczekiwać, że funkcja wywołania zwrotnego zostanie wywołana w ramach danego kadru. Jeśli chcesz, aby była wykonywana w określonym przedziale czasu, użyj limitu czasu.
  • Czy mogę mieć kilka wywołań zwrotnych requestIdleCallback? Tak, możesz, ale możesz mieć wiele wywołań zwrotnych requestAnimationFrame. Warto jednak pamiętać, że jeśli czas trwania pierwszego połączenia zwrotnego zostanie wykorzystany, nie będzie już czasu na inne połączenia zwrotne. Inne wywołania zwrotne będą musiały poczekać, aż przeglądarka będzie nieaktywna, zanim zostaną uruchomione. W zależności od tego, co chcesz zrobić, lepiej może być użyć jednej funkcji wywołania zwrotnego w trybie bezczynności i podzielić na nią zadania. 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? Nowy wywołanie zwrotne w przypadku bezczynności zostanie zaplanowane do wykonania tak szybko, jak to możliwe, począwszy od następnego (a nie bieżącego) obrazu.

Bezczynny.

requestIdleCallback to świetny sposób na uruchomienie kodu bez przeszkadzania użytkownikowi. Jest ona prosta w użyciu i bardzo elastyczna. 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.