Korzystanie z requestIdleCallback

Wiele witryn i aplikacji ma wiele skryptów do wykonania. JavaScript często musi zostać uruchomiony jak najszybciej, ale jednocześnie nie chcesz, by przeszkadzał użytkownikowi. Jeśli wysyłasz dane analityczne, gdy użytkownik przewija stronę, lub dołączasz do modelu DOM elementy, które są po kliknięciu przycisku, Twoja aplikacja internetowa może przestać reagować, co źle wpłynie na wrażenia użytkownika.

Użycie requestIdleCallback do zaplanowania opcjonalnych zadań.

Dobra wiadomość jest taka, że teraz istnieje interfejs API, który może Ci w tym pomóc: requestIdleCallback. W ten sam sposób, jak zastosowanie funkcji requestAnimationFrame pozwoliło nam poprawnie planować animacje i zmaksymalizować szanse na uzyskanie 60 kl./s, requestIdleCallback planuje pracę na czas wolny na końcu klatki lub gdy użytkownik jest nieaktywny. Oznacza to, że możesz wykonać swoją pracę, nie przeszkadzając użytkownikowi. Jest ona dostępna od wersji Chrome 47, więc możesz ją wypróbować już dziś, używając Chrome Canary. To funkcja eksperymentalna, a jej parametry wciąż się zmieniają, więc w przyszłości może to ulec zmianie.

Dlaczego warto korzystać z metody requestIdleCallback?

Samodzielne zaplanowanie mniej ważnych zadań jest bardzo trudne. Nie można dokładnie określić, ile czasu pozostało do końca renderowania klatek, ponieważ po wykonaniu wywołań zwrotnych requestAnimationFrame trzeba wykonać jeszcze obliczenia związane ze stylem, układem, renderowaniem i innymi właściwościami przeglądarki. Rozwiązanie reklam wyświetlanych w aplikacji nie uwzględnia żadnej z tych sytuacji. Aby mieć pewność, że użytkownik nie wchodzi w interakcję w jakiś sposób, musisz też przypisywać słuchaczy do każdego rodzaju zdarzenia interakcji (scroll, touch, click), nawet jeśli nie są one potrzebne do obsługi funkcji, tylko tak, aby mieć absolutną pewność, że użytkownik nie wchodzi w interakcję. Z drugiej strony przeglądarka wie dokładnie, ile czasu zostało do końca klatki i czy użytkownik wchodzi w interakcję z reklamą. Dzięki temu requestIdleCallback uzyskujemy interfejs API, który pozwala nam jak najefektywniej wykorzystać wolny czas.

Przyjrzyjmy się bliżej tej funkcji i zastanówmy się, jak możemy ją wykorzystać.

Sprawdzam żądanie requestIdleCallback

Usługa requestIdleCallback jest stosunkowo młoda, dlatego zanim z niej skorzystasz, sprawdź, czy jest już dostępna:

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

Możesz też podświetlić jego działanie, które 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);
    }

Używanie funkcji setTimeout nie jest zbyt dobre, bo nie wie o czasie bezczynności, tak jak w przypadku requestIdleCallback, ale skoro funkcja ta zostałaby wywołana bezpośrednio, gdyby funkcja requestIdleCallback była niedostępna, to ten sposób nie jest gorszy w problemie. Jeśli masz podkładkę requestIdleCallback, połączenia będą przekierowywane dyskretnie, co bardzo dobrze.

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

Korzystanie z requestIdleCallback

Wywołanie requestIdleCallback jest bardzo podobne do wywołania requestAnimationFrame, ponieważ pierwszym parametrem jest funkcja wywołania zwrotnego:

requestIdleCallback(myNonEssentialWork);

Wywołanie funkcji myNonEssentialWork otrzymuje obiekt deadline zawierający funkcję, która zwraca liczbę wskazującą pozostały czas pracy:

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

Funkcję timeRemaining można wywołać, aby uzyskać najnowszą wartość. Gdy timeRemaining() zwraca zero, 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);
}

Gwarantowanie wywołania funkcji

Co robisz, gdy jest dużo pracy? Możliwe, że obawiasz się, że Twoje wywołanie zwrotne nigdy nie zostanie zrealizowane. Mimo że requestIdleCallback przypomina requestAnimationFrame, różni się też tym, że zawiera opcjonalny drugi parametr: obiekt opcji z właściwością aTimeout. Ten limit czasu (jeśli został ustawiony) daje przeglądarce czas (w milisekundach), do którego musi wykonać wywołanie zwrotne:

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

Jeśli wywołanie zwrotne jest wykonywane z powodu przekroczenia limitu czasu, zauważysz 2 rzeczy:

  • timeRemaining() zwraca wartość zero.
  • Właściwość didTimeout obiektu deadline ma wartość prawda.

Jeśli zobaczysz, że didTimeout to prawda, najprawdopodobniej będziesz po prostu uruchomić pracę i ją wykonać:

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 korzystanie z tego limitu czasu może powodować problemy użytkowników (praca może spowodować, że aplikacja przestanie odpowiadać lub będzie działać nieprawidłowo). Zachowaj ostrożność podczas ustawiania tego parametru. Jeśli możesz, pozwól przeglądarce zdecydować, kiedy ma oddzwonić.

Używanie requestIdleCallback do wysyłania danych analitycznych

Przyjrzyjmy się możliwości wysyłania danych analitycznych za pomocą requestIdleCallback. W takim przypadku lepiej będzie śledzić zdarzenie takie jak np. kliknięcie menu nawigacyjnego. Ponieważ jednak zwykle są one animowane na ekranie, lepiej nie wysyłać tego zdarzenia od razu do Google Analytics. Utworzymy tablicę zdarzeń do wysłania i zażądaj ich wysłania 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 będziemy potrzebować requestIdleCallback do przetwarzania oczekujących wydarzeń:

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ć, ustawiłem limit czasu na 2 sekundy, ale jego wartość zależy od aplikacji. W przypadku danych analitycznych ma sens, aby po przekroczeniu limitu czasu dane były raportowane w rozsądnym terminie, a nie tylko w pewnym 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 requestIdleCallback nie istnieje, dane analityczne powinny zostać wysłane natychmiast. W aplikacji produkcyjnej lepiej jednak byłoby opóźnić wysyłanie, podając limit czasu, aby nie kolidować z żadnymi interakcjami i powodować zacinanie się.

Wprowadzanie zmian DOM za pomocą requestIdleCallback

Kolejną sytuacją, w której element requestIdleCallback może bardzo pomóc w zwiększeniu wydajności, jest wprowadzenie nieistotnych zmian DOM, takich jak dodanie elementów na końcu stale rosnącej, leniwej listy. Zobaczmy, jak requestIdleCallback w rzeczywistości mieści się w typowym kadrze.

Typowa klatka.

Możliwe, że przeglądarka jest zbyt zajęta, aby wykonywać wywołania zwrotne w danej klatce, więc nie spodziewaj się, że na końcu klatki pojawi się dowolny czas wolny na dalsze wykonywanie pracy. To sprawia, że jest to coś w rodzaju czegoś takiego jak setImmediate, które uruchamia w każdej klatce.

Jeśli wywołanie zwrotne zostanie uruchomione na końcu klatki, zostanie zaplanowane na zakończenie bieżącej klatki, co oznacza, że zmiany stylu zostaną zastosowane, a co ważniejsze – układ zostanie obliczony. Jeśli wprowadzimy zmiany DOM w nieaktywnym wywołaniu zwrotnym, obliczenia układu zostaną unieważnione. Jeśli w następnej ramce jest odczyt układu, np.getBoundingClientRect, clientWidth itp., przeglądarka musi użyć wymuszonego układu synchronicznego, co może ograniczać wydajność.

Innym powodem, dla którego nie wywołują zmian DOM w nieaktywnym wywołaniu zwrotnym, jest to, że wpływ zmiany DOM jest nieprzewidywalny i dlatego łatwo można było wyprzedzić czas określony przez przeglądarkę.

Sprawdzoną metodą jest wprowadzanie zmian DOM tylko w wywołaniu zwrotnym requestAnimationFrame, ponieważ planowanie jest planowane przez przeglądarkę z uwzględnieniem tego typu pracy. Oznacza to, że w naszym kodzie musi znajdować się fragment dokumentu, który można dołączyć w kolejnym wywołaniu zwrotnym requestAnimationFrame. Jeśli używasz biblioteki VDOM, do wprowadzania zmian użyjesz polecenia requestIdleCallback, ale zastosuj poprawki DOM w następnym wywołaniu zwrotnym requestAnimationFrame, a nie nieaktywne wywołanie zwrotne.

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 do niego właściwości textContent, ale możliwe, że Twój kod tworzenia elementu miałby większe znaczenie. Po utworzeniu elementu zostaje wywołany element scheduleVisualUpdateIfNeeded, który konfiguruje pojedyncze wywołanie zwrotne requestAnimationFrame, które z kolei dołącza 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;
}

Od teraz podczas dołączania elementów do DOM nie będzie problemów. Znakomity

Najczęstsze pytania

  • Czy jest dostępny kod polyfill? Nie. Jest jednak podkładka, jeśli chcesz mieć przejrzyste przekierowanie do setTimeout. Ten interfejs API istnieje, ponieważ tworzy bardzo realną lukę w platformie internetowej. Trudno wywnioskować brak aktywności, ale nie istnieją interfejsy API JavaScript do określania ilości wolnego czasu na końcu klatki, więc najlepiej je domyślać. Interfejsy API takie jak setTimeout, setInterval czy setImmediate można wykorzystać do planowania pracy, ale nie działają one tak, aby uniknąć interakcji użytkownika w taki sposób, w jaki jest requestIdleCallback.
  • Co się stanie, jeśli przekroczę termin? Jeśli timeRemaining() zwróci wartość zero, ale zdecydujesz się wyświetlać reklamy dłużej, możesz to zrobić bez obaw, że przeglądarka przerwie pracę. Przeglądarka musi jednak podać termin, aby zadbać o satysfakcję użytkowników, dlatego musisz zawsze przestrzegać terminu, chyba że masz bardzo ważny powód.
  • Czy istnieje maksymalna wartość, którą zwróci timeRemaining()? Tak, obecnie jest to 50 ms. Jeśli chcesz utrzymać elastyczność aplikacji, wszystkie odpowiedzi na interakcje użytkowników nie powinny przekraczać 100 ms. Jeśli użytkownik wejdzie w interakcję z aplikacją, w większości przypadków okno 50 ms powinno umożliwiać zakończenie nieaktywnego wywołania zwrotnego i reagowanie przeglądarki na interakcje użytkownika. Możesz otrzymać kilka zaplanowanych wywołań zwrotnych nieaktywnego produktu (jeśli przeglądarka określi, że mamy wystarczająco dużo czasu na ich wykonanie).
  • Czy jest coś, czego nie należy wykonywać w ramach requestIdleCallback? Najlepiej wykonywać zadania na małych fragmentach (mikrozadaniach), które mają stosunkowo przewidywalne cechy. Na przykład zmiana DOM będzie miała nieprzewidywalny czas wykonywania, ponieważ uruchamia 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 opisany powyżej. Trzeba też pamiętać o realizowaniu (lub odrzucaniu) obietnic, ponieważ wywołania zwrotne są wykonywane natychmiast po zakończeniu nieaktywnego wywołania zwrotnego, nawet jeśli został osiągnięty limit czasu.
  • Czy zawsze pojawi się requestIdleCallback na końcu klatki? Nie, nie zawsze. Przeglądarka zaplanuje wywołanie zwrotne, gdy na końcu klatki zostanie wolne miejsce lub w okresach braku aktywności użytkownika. Wywołanie zwrotne nie powinno być wywoływane w każdej klatce. Jeśli chcesz, aby zostało wykonane w określonym przedziale czasu, musisz wykorzystać limit czasu.
  • Czy mogę mieć kilka wywołań zwrotnych requestIdleCallback? Tak, możesz mieć wiele wywołań zwrotnych requestAnimationFrame. Warto jednak pamiętać, że jeśli pierwsze wywołanie zwrotne w czasie, po którym oddzwonimy, na pierwsze wywołanie zwrotne, na pierwsze wywołanie zwrotne, to nie będzie już więcej czasu na kolejne wywołania. Pozostałe wywołania zwrotne będą musiały czekać do kolejnego bezczynności przeglądarki, zanim będą mogły zostać uruchomione. Zależnie od tego, jaką pracę chcesz wykonać, lepiej jest jedno nieaktywne wywołanie zwrotne i podzielić swoją pracę. Możesz też wykorzystać limit czasu, aby na pewno nie stracić czasu na żadne wywołania.
  • Co się stanie, jeśli skonfiguruję nowe nieaktywne wywołanie zwrotne w innym? Nowe nieaktywne wywołanie zwrotne zostanie zaplanowane tak, jak to możliwe, zaczynając od następnej klatki (a nie od bieżącej).

Brak aktywności!

requestIdleCallback to świetny sposób, by upewnić się, że możesz uruchomić kod bez przeszkadzania użytkownikowi. Jest prosty w obsłudze i bardzo elastyczny. Jednak wciąż jesteśmy na początku drogi, a specyfikacja nie została w pełni ustalona, więc chętnie poznamy Twoją opinię.

Wypróbuj tę funkcję w Chrome Canary, wypróbuj ją w swoich projektach i daj nam znać, jak Ci poszło.