Utilizzo di requestIdleCallback

Paul Lewis

Molti siti e app devono eseguire molti script. Spesso, JavaScript deve essere eseguito il prima possibile, ma allo stesso tempo non vuoi che intralci l'utente. Se invii dati di analisi quando l'utente scorre la pagina o aggiungi elementi al DOM mentre questi toccano il pulsante, l'app web potrebbe non rispondere e causare un'esperienza utente scadente.

Utilizzo di requestIdleCallback per pianificare il lavoro non essenziale.

La buona notizia è che ora esiste un'API in grado di aiutarti: requestIdleCallback. Allo stesso modo in cui l'adozione di requestAnimationFrame ci ha permesso di pianificare correttamente le animazioni e massimizzare le possibilità di raggiungere i 60 f/s, requestIdleCallback pianificherà il funzionamento quando c'è tempo libero alla fine di un frame o quando l'utente è inattivo. Ciò significa che esiste l'opportunità di svolgere il tuo lavoro senza essere d'intralcio. È disponibile a partire da Chrome 47, quindi puoi fare un turbinio oggi stesso utilizzando Chrome Canary. Si tratta di una funzionalità sperimentale e le specifiche sono ancora in continua evoluzione, quindi le cose potrebbero cambiare in futuro.

Perché dovrei usare requestIdleCallback?

Pianificare in autonomia i lavori non essenziali è molto difficile. È impossibile capire esattamente quanto tempo di frame rimane perché dopo l'esecuzione dei callback requestAnimationFrame, devono essere eseguiti calcoli di stile, layout, colorazione e altri componenti interni del browser. Una soluzione home-roll non può tenere conto di nessuno di questi. Per assicurarti che un utente non interagisca in qualche modo, devi anche collegare i listener a ogni tipo di evento di interazione (scroll, touch, click), anche se non ne hai bisogno per la funzionalità, solo in modo da avere la certezza assoluta che l'utente non stia interagendo. Il browser, invece, sa esattamente quanto tempo è disponibile alla fine del frame e se l'utente sta interagendo, quindi attraverso requestIdleCallback otteniamo un'API che ci consente di utilizzare il tempo libero nel modo più efficiente possibile.

Diamo un’occhiata più da vicino e vediamo come possiamo utilizzarlo.

Controllo di requestIdleCallback in corso...

requestIdleCallback è ancora agli inizi, quindi prima di utilizzarlo devi verificare che possa essere usato:

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

Puoi anche ridurne il comportamento, che richiede di tornare a 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);
    }

L'utilizzo di setTimeout non è l'ideale perché non consente di conoscere il tempo di inattività come fa requestIdleCallback, ma dal momento che chiameresti direttamente la funzione se requestIdleCallback non fosse disponibile, questa impostazione non andrebbe peggio di ridurre lo shimm in questo modo. Grazie allo shim, se requestIdleCallback sarà disponibile, le tue chiamate verranno reindirizzate senza audio, il che è fantastico.

Per ora, però, supponiamo che esista.

Utilizzo di requestIdleCallback

La chiamata di requestIdleCallback è molto simile a quella di requestAnimationFrame, in quanto prende una funzione di callback come primo parametro:

requestIdleCallback(myNonEssentialWork);

Quando myNonEssentialWork viene chiamato, gli verrà assegnato un oggetto deadline contenente una funzione che restituisce un numero che indica il tempo rimanente per il lavoro:

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

La funzione timeRemaining può essere chiamata per ottenere il valore più recente. Quando timeRemaining() restituisce zero, puoi programmare un altro requestIdleCallback se hai ancora molto lavoro da fare:

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

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

Garantire che la funzione venga chiamata

Cosa si fa se le cose sono molto impegnate? Potresti temere che il tuo interlocutore non venga mai richiamato. Anche se requestIdleCallback assomiglia a requestAnimationFrame, differisce anche dal fatto che utilizza un secondo parametro facoltativo: un oggetto opzioni con una proprietà timeout. Questo timeout, se impostato, concede al browser un tempo in millisecondi entro il quale deve eseguire il callback:

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

Se il callback viene eseguito a causa dell'attivazione del timeout, noterai due cose:

  • timeRemaining() restituirà zero.
  • La proprietà didTimeout dell'oggetto deadline sarà true.

Se vedi che il didTimeout è vero, molto probabilmente vorrai eseguire il lavoro e finire qui:

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

A causa della potenziale interruzione che questo timeout può causare ai tuoi utenti (il lavoro potrebbe far sì che l'app non risponda o risulti insolita), fai attenzione all'impostazione di questo parametro. Dove puoi, lascia che sia il browser a decidere quando chiamare il callback.

Utilizzo di requestIdleCallback per l'invio dei dati di analisi

Diamo un'occhiata a come usare requestIdleCallback per inviare i dati di analisi. In questo caso, probabilmente vorrai monitorare un evento come, ad esempio, il tocco su un menu di navigazione. Tuttavia, poiché normalmente si animano sullo schermo, vorremo evitare di inviare immediatamente questo evento a Google Analytics. Verrà creato un array di eventi da inviare per richiedere che vengano inviati in un determinato momento:

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

Ora dovremo utilizzare requestIdleCallback per elaborare gli eventuali eventi in sospeso:

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

Qui puoi vedere che ho impostato un timeout di 2 secondi, ma questo valore dipende dall'applicazione. Per i dati di analisi, ha senso utilizzare un timeout per garantire che i dati vengano riportati in un periodo di tempo ragionevole anziché solo in un determinato momento in futuro.

Infine dobbiamo scrivere la funzione che verrà eseguita da 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();
}

Per questo esempio, ho presupposto che, in assenza di requestIdleCallback, i dati di analisi dovessero essere inviati immediatamente. In un'applicazione di produzione, tuttavia, sarebbe probabilmente meglio ritardare l'invio con un timeout per assicurarsi che non entri in conflitto con le interazioni e causi jank.

Utilizzo di requestIdleCallback per apportare modifiche al DOM

Un'altra situazione in cui requestIdleCallback può davvero migliorare le prestazioni è quando devi apportare modifiche non essenziali al DOM, ad esempio l'aggiunta di elementi alla fine di un elenco in continua crescita caricato con il caricamento lento. Vediamo come si inserisce requestIdleCallback in un frame standard.

Un frame tipico.

È possibile che il browser sia troppo occupato per eseguire qualsiasi callback in un determinato frame, quindi non aspettarti che ci sia qualsiasi tempo libero alla fine di un frame per svolgere altro lavoro. Ciò lo rende diverso da qualcosa come setImmediate, che viene eseguito per frame.

Se il callback viene attivato alla fine del frame, verrà pianificato per essere eseguito dopo il commit del frame corrente, il che significa che le modifiche allo stile saranno state applicate e, soprattutto, al calcolo del layout. Se apportiamo modifiche al DOM all'interno del callback di inattività, i calcoli del layout verranno invalidati. Se nel frame successivo è presente un qualsiasi tipo di lettura del layout, ad esempio getBoundingClientRect, clientWidth e così via, il browser dovrà eseguire un layout sincrono forzato, che rappresenta un potenziale collo di bottiglia delle prestazioni.

Un altro motivo per cui non si attivano modifiche del DOM nel callback di inattività è che l'impatto temporale della modifica del DOM è imprevedibile e, di conseguenza, potremmo facilmente superare la scadenza fornita dal browser.

La best practice è apportare modifiche al DOM solo all'interno di un callback requestAnimationFrame, poiché è pianificato dal browser in base a quel tipo di lavoro. Ciò significa che il nostro codice dovrà utilizzare un frammento di documento, che potrà essere aggiunto nel prossimo callback requestAnimationFrame. Se usi una libreria VDOM, utilizzerai requestIdleCallback per apportare modifiche, ma applicherai le patch DOM nel callback requestAnimationFrame successivo, non nel callback di inattività.

Teniamo conto di questo, diamo un'occhiata al codice:

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

Qui creo l'elemento e uso la proprietà textContent per completarlo, ma è probabile che il codice di creazione dell'elemento sia più coinvolto. Dopo aver creato l'elemento scheduleVisualUpdateIfNeeded, viene richiesto di configurare un singolo callback requestAnimationFrame che aggiungerà, a sua volta, il frammento del documento al corpo:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

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

Se tutto sta bene, ora noteremo molto meno blocchi quando si aggiungeranno elementi al DOM. Eccellente

Domande frequenti

  • È presente un polyfill? Purtroppo no, ma è presente uno shim se vuoi avere un reindirizzamento trasparente a setTimeout. Il motivo per cui esiste questa API è che colma un divario molto reale nella piattaforma web. Dedurre una mancanza di attività è difficile, ma non esistono API JavaScript per determinare la quantità di tempo libero alla fine del frame, quindi nella migliore delle ipotesi è necessario fare delle ipotesi. API come setTimeout, setInterval o setImmediate possono essere utilizzate per pianificare il lavoro, ma non sono in tempo per evitare l'interazione dell'utente come requestIdleCallback.
  • Cosa succede se supero la scadenza? Se timeRemaining() restituisce zero, ma decidi di eseguire un'esecuzione più lunga, puoi farlo senza temere che il browser blocchi il tuo lavoro. Tuttavia, il browser indica una scadenza per provare a garantire un'esperienza senza problemi agli utenti, quindi, a meno che non ci sia un motivo valido, dovresti sempre rispettarla.
  • Esiste un valore massimo che verrà restituito da timeRemaining()? Sì, attualmente dura 50 ms. Quando cerchi di gestire un'applicazione reattiva, tutte le risposte alle interazioni degli utenti devono essere mantenute al di sotto di 100 ms. Se l'utente interagisce, nella maggior parte dei casi, la finestra di 50 ms dovrebbe consentire il completamento del callback di inattività e il browser di rispondere alle interazioni dell'utente. Potresti ricevere più callback di inattività programmati uno dopo l'altro (se il browser determina che c'è tempo sufficiente per eseguirli).
  • C'è qualche tipo di lavoro che non dovrei fare in una richiestaIdleCallback? Idealmente, il lavoro da svolgere dovrebbe essere suddiviso in piccole porzioni (microtask) con caratteristiche relativamente prevedibili. Ad esempio, modificare il DOM in particolare avrà tempi di esecuzione imprevedibili, poiché attiverà calcoli di stile, layout, colorazione e compositing. Di conseguenza, dovresti apportare modifiche al DOM solo in un callback requestAnimationFrame come suggerito sopra. Un'altra cosa di cui diffidare è risolvere (o rifiutare) le promesse, poiché i callback verranno eseguiti subito dopo il termine del callback inattivo, anche se non rimane altro tempo.
  • Riceverò sempre un requestIdleCallback alla fine di un frame? No, non sempre. Il browser pianificherà il callback ogni volta che è disponibile del tempo alla fine di un frame o in periodi in cui l'utente non è attivo. Non aspettarti che il callback venga chiamato per ogni frame e, se hai bisogno che venga eseguito entro un determinato periodo di tempo, dovresti utilizzare il timeout.
  • Posso avere più richiamate requestIdleCallback? Sì, puoi, come puoi fare, più volte puoi richiamare requestAnimationFrame. Tuttavia, vale la pena ricordare che se la prima volta che viene richiamato il sistema esaurisce il tempo restante, non ci sarà più tempo per le altre chiamate. Gli altri callback dovranno quindi attendere che il browser diventi inattivo prima di poter essere eseguiti. A seconda del lavoro che stai cercando di svolgere, potrebbe essere meglio avere un singolo callback di inattività e dividere il lavoro al suo interno. In alternativa, puoi utilizzare il timeout per assicurarti che nessun callback venga interrotto.
  • Cosa succede se imposto un nuovo callback di inattività all'interno di un altro? Il nuovo callback di inattività verrà programmato per essere eseguito il prima possibile, a partire dal frame successivo (anziché quello attuale).

In pausa!

requestIdleCallback è un modo eccezionale per assicurarti di poter eseguire il tuo codice, senza intralciare l'utente. È semplice da usare e molto flessibile. Tuttavia, siamo ancora all'inizio e le specifiche non sono ancora finalizzate, quindi qualsiasi tuo feedback è benvenuto.

Provala in Chrome Canary, prova i tuoi progetti e facci sapere cosa ne pensi.