Korzystanie z requestIdleCallback

Wiele witryn i aplikacji ma do wykonania wiele skryptów. Kod JavaScript musi być często wykonywany 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. Podobnie jak w przypadku requestAnimationFrame, dzięki użyciu requestIdleCallback możemy prawidłowo zaplanować animacje i zmaksymalizować szanse na osiągnięcie 60 FPS. requestIdleCallback będzie planować pracę, gdy w ramach danego kadru będzie wolny czas lub gdy użytkownik będzie nieaktywny. Oznacza to, że możesz wykonywać swoją pracę bez przeszkadzania użytkownikowi. Jest ona dostępna od wersji Chrome 47, więc możesz ją wypróbować już dziś w Chrome Canary. Jest to funkcja eksperymentalna, a 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, które muszą zostać wykonane. Rozwiązanie opracowane samodzielnie nie uwzględnia żadnego z tych elementó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 temu interfejsowi API możemy wykorzystać dowolny dostępny czas w najbardziej efektywny sposób.requestIdleCallback

Przyjrzyjmy się mu bliżej i zobaczmy, jak możemy go wykorzystać.

Sprawdzanie, czy istnieje funkcja requestIdleCallback

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

Użycie funkcji setTimeout nie jest najlepszym rozwiązaniem, ponieważ nie wie ona o czasie bezczynności, tak jak funkcja requestIdleCallback. Jednak jeśli funkcja requestIdleCallback nie byłaby dostępna, musiałbyś wywołać funkcję bezpośrednio, więc w tym przypadku użycie funkcji zastępczej nie jest gorszym rozwiązaniem. Dzięki temu, jeśli requestIdleCallback będzie dostępny, Twoje połączenia będą automatycznie przekierowywane, co jest bardzo wygodne.

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

Używanie funkcji requestIdleCallback

Wywoływanie funkcji requestIdleCallback jest bardzo podobne do wywoływania 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();
}

Aby uzyskać najnowszą wartość, można wywołać funkcję timeRemaining. Gdy funkcja timeRemaining() zwróci wartość 0, możesz zaplanować kolejne wywołanie funkcji 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? Możesz się obawiać, że nie otrzymasz odpowiedzi. Chociaż funkcja requestIdleCallback przypomina funkcję requestAnimationFrame, różni się od niej tym, że przyjmuje opcjonalny drugi parametr: obiekt opcji z właściwością czasu oczekiwania. 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 uruchomienia 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 wykonać zadanie:

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 wykonać połączenie zwrotne.

Używanie funkcji requestIdleCallback do wysyłania danych analitycznych

Zobaczmy, jak działa wysyłanie danych analitycznych za pomocą tagu requestIdleCallback. 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();
    }
}

Tutaj ustawiłem limit czasu na 2 sekundy, ale ta wartość zależy 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. W wersji produkcyjnej lepiej jednak opóźnić wysyłanie, aby nie kolidowało to z żadnymi interakcjami i nie powodowało zacięć.

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 wywołać jakiekolwiek funkcje zwracające wartości z powrotem w danym interwale, więc nie należy oczekiwać, że na końcu tego interwału będzie żaden wolny czas na wykonanie dodatkowych zadań. 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 wprowadzimy zmiany w DOM-ie w ramach wywołania zwrotnego w stan nieaktywności, 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 podany przez przeglądarkę.

Sprawdzoną metodą jest wprowadzanie zmian w DOM tylko w ramach wywołania zwrotnego requestAnimationFrame, ponieważ jest ono zaplanowane przez przeglądarkę z myślą o takim typie pracy. Oznacza to, że nasz kod będzie musiał 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, przyjrzyjmy się kodowi:

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

Jeśli wszystko pójdzie dobrze, dodawanie elementów do DOM będzie teraz znacznie płynniejsze. Znakomity

Najczęstsze pytania

  • Czy jest dostępna polyfill? Niestety nie, ale jeśli chcesz mieć przezroczyste przekierowanie do setTimeout, możesz użyć shim. 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 uruchomienie, możesz to zrobić bez obaw, ż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 podzielić pracę na małe fragmenty (mikrozadania) o względnie przewidywalnych właściwościach. 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 requestIdleCallback? 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 przypadku każdego obrazu. 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, ponieważ możesz ustawić 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 muszą poczekać, aż przeglądarka będzie nieaktywna, zanim zostaną uruchomione. W zależności od tego, co chcesz zrobić, lepiej może być użycie jednej funkcji wywołania zwrotnego w trybie bezczynności i podzielenie pracy na nią. Możesz też użyć limitu czasu, aby mieć pewność, że żadne wywołania zwrotne nie zostaną odrzucone z powodu braku czasu.
  • Co się stanie, jeśli ustawię nowe wywołanie zwrotne w stanie bezczynności wewnątrz innego wywołania? 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 dopiero początek, a specyfikacja nie jest jeszcze w pełni ustalona, więc chętnie poznamy Twoją opinię.

Sprawdź tę funkcję w Chrome Canary, wykorzystaj ją w swoich projektach i daj nam znać, jak Ci się podobała.