Utilizzo di requestIdleCallback

Paul Lewis

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

Utilizzo di requestIdleCallback per pianificare il lavoro non essenziale.

La buona notizia è che ora c'è un'API che può aiutarti: requestIdleCallback. Allo stesso modo in cui l'adozione di requestAnimationFrame ci ha permesso di programmare le animazioni correttamente e di massimizzare le possibilità di raggiungere i 60 f/s, requestIdleCallback programma il lavoro quando c'è tempo libero alla fine di un fotogramma o quando l'utente è inattivo. Ciò significa che hai l'opportunità di svolgere il tuo lavoro senza interferire con l'utente. È disponibile a partire da Chrome 47, quindi puoi provarlo oggi stesso utilizzando Chrome Canary. Si tratta di una funzionalità sperimentale e le specifiche sono ancora in fase sperimentale, quindi le cose potrebbero cambiare in futuro.

Perché dovrei utilizzare requestIdleCallback?

Non è facile pianificare autonomamente le attività non essenziali. È impossibile capire esattamente quanto tempo di frame rimane perché dopo l'esecuzione dei callback requestAnimationFrame ci sono calcoli di stile, layout, colorazione e altri elementi interni del browser che devono essere eseguiti. Una soluzione offerta in casa non può tenerne conto. Per assicurarti che un utente non interagisca in qualche modo, devi anche collegare gli ascoltatori a ogni tipo di evento di interazione (scroll, touch, click), anche se non sono necessari per la funzionalità, solo per avere la certezza che l'utente non stia interagendo. Il browser, invece, sa esattamente quanto tempo è a disposizione alla fine del frame e se l'utente sta interagendo. Quindi, tramite requestIdleCallback otteniamo un'API che ci permette di utilizzare tutto 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 verifica che sia disponibile per l'uso:

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

Puoi anche ridurne il comportamento, il che richiede il ricorso 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 è un ottimo modo perché non conosce il tempo di inattività come fa requestIdleCallback, ma poiché chiamare direttamente la tua funzione se requestIdleCallback non era disponibile, non c'è niente di peggio di questo tipo di shimming. Con lo shim, se requestIdleCallback è disponibile, le tue chiamate verranno reindirizzate in modalità silenziosa, il che è perfetto.

Per ora, supponiamo che esista.

Utilizzo di requestIdleCallback

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

requestIdleCallback(myNonEssentialWork);

Quando myNonEssentialWork viene chiamato, gli verrà assegnato un oggetto deadline che contiene una funzione che restituisce un numero che indica quanto tempo rimane per il tuo lavoro:

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

È possibile chiamare la funzione timeRemaining per ottenere il valore più recente. Quando timeRemaining() restituisce zero, puoi pianificare un'altra requestIdleCallback se hai ancora altro lavoro da fare:

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

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

Garantire che la funzione sia denominata

Cosa fate se ci sono molti impegni? Forse temi che il tuo callback non venga mai chiamato. Sebbene requestIdleCallback sia simile a requestAnimationFrame, si differenzia anche per il fatto che richiede 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 la callback viene eseguita a causa dell'attivazione del timeout, noterai due cose:

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

Se scopri che didTimeout è vero, è molto probabile che vorrai semplicemente continuare a lavorare e finire di utilizzarlo:

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, questo timeout può causare ai tuoi utenti (il lavoro potrebbe far sì che la tua app non risponda o risulti inadeguato) imposta questo parametro con cautela. 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 all'utilizzo di requestIdleCallback per inviare i dati di analisi. In questo caso, probabilmente vorrai monitorare un evento come, ad esempio, toccando un menu di navigazione. Tuttavia, poiché normalmente si animano sullo schermo, eviteremo di inviare immediatamente questo evento a Google Analytics. Creeremo un array di eventi da inviare e richiedere che vengano inviati a un certo punto in futuro:

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 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 dipenderà dalla tua applicazione. Per i dati di analisi, ha senso che venga utilizzato un timeout per garantire che i dati vengano registrati in un periodo di tempo ragionevole anziché solo in un determinato momento nel futuro.

Infine dobbiamo scrivere la funzione che requestIdleCallback eseguirà.

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 pensato che, se requestIdleCallback non esisteva, 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 alcuna interazione e non 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 nel DOM non essenziali, ad esempio l'aggiunta di elementi alla fine di un elenco in continuo aumento mediante caricamento lento. Vediamo come requestIdleCallback si inserisce in una cornice tipica.

Un frame tipico.

È possibile che il browser sia troppo occupato per eseguire callback in un determinato frame, quindi non aspettarti al termine del tempo libero per eseguire altre operazioni. Questo lo rende diverso da qualcosa come setImmediate, che viene eseguito per frame.

Se il callback viene attivato alla fine del frame, verrà programmato per andare dopo il commit del frame corrente, il che significa che le modifiche di stile saranno state applicate e, soprattutto, calcolato il layout. Se apportiamo modifiche nel DOM all'interno del callback di inattività, questi calcoli del layout vengono invalidati. Se è presente un qualsiasi tipo di lettura del layout nel frame successivo, 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 vengono attivate le modifiche del DOM nel callback di inattività è che l'impatto in termini di tempo della modifica del DOM è imprevedibile e, come tale, potremmo facilmente superare la scadenza fornita dal browser.

La best practice prevede di apportare modifiche DOM solo all'interno di un callback requestAnimationFrame, poiché viene pianificato dal browser tenendo a mente questo tipo di lavoro. Ciò significa che per il nostro codice sarà necessario utilizzare un frammento di documento, che potrà essere aggiunto al successivo callback requestAnimationFrame. Se usi una libreria VDOM, utilizzerai requestIdleCallback per apportare modifiche, ma applicherai le patch DOM nel callback requestAnimationFrame successivo, non nel callback inattivo.

Tenendo presente questo aspetto, 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 compilarlo, ma è probabile che il codice di creazione dell'elemento sia più complesso. Dopo aver creato l'elemento scheduleVisualUpdateIfNeeded viene chiamato, che imposterà un singolo callback requestAnimationFrame che, a sua volta, aggiungerà il frammento di 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;
}

Bene, ora vedremo molto meno jank quando aggiungi elementi al DOM. Eccellente

Domande frequenti

  • È presente un polyfill? Purtroppo no, ma c'è uno shim se vuoi avere un reindirizzamento trasparente a setTimeout. Il motivo per cui questa API esiste è perché colma una lacuna 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 devi provare a indovinare. API come setTimeout, setInterval o setImmediate possono essere utilizzate per pianificare il lavoro, ma non hanno il tempo di evitare l'interazione dell'utente come accade con requestIdleCallback.
  • Che cosa succede se non supero la scadenza? Se timeRemaining() restituisce zero, ma decidi di eseguire l'esecuzione per un periodo più lungo, puoi farlo senza temere che il browser blocchi il tuo lavoro. Tuttavia, il browser indica il termine ultimo per provare a garantire un'esperienza ottimale agli utenti. Pertanto, a meno che non ci sia un motivo valido, dovresti sempre rispettare la scadenza.
  • Esiste un valore massimo che viene restituito da timeRemaining()? Sì, al momento sono 50 ms. Quando si cerca di mantenere un'applicazione reattiva, tutte le risposte alle interazioni degli utenti devono essere inferiori a 100 ms. Se l'utente interagisce, la finestra di 50 ms dovrebbe, nella maggior parte dei casi, consentire il completamento del callback di inattività e il browser di rispondere alle interazioni dell'utente. Potresti ottenere più callback di inattività pianificati in sequenza (se il browser stabilisce che c'è tempo sufficiente per eseguirli).
  • Esiste un tipo di lavoro che non devo fare in una richiesta requestIdleCallback? Idealmente, il lavoro da svolgere dovrebbe essere suddiviso in piccoli blocchi (micro attività) con caratteristiche relativamente prevedibili. Ad esempio, la modifica del DOM in particolare ha tempi di esecuzione imprevedibili, poiché attiverà calcoli di stile, layout, pittura e compositing. Di conseguenza, devi apportare solo modifiche DOM in un callback requestAnimationFrame, come suggerito in precedenza. Un'altra cosa di cui fare attenzione è risolvere (o rifiutare) le promesse, poiché i callback vengono eseguiti immediatamente dopo il termine del callback di inattività, anche se non rimane più tempo.
  • Riceverò sempre un requestIdleCallback alla fine di un frame? No, non sempre. Il browser pianifica il callback ogni volta che c'è del tempo libero alla fine di un frame o nei periodi in cui l'utente non è attivo. Non dovresti aspettarti che il callback venga chiamato per ogni frame e se hai bisogno di eseguirlo entro un determinato periodo di tempo devi utilizzare il timeout.
  • Posso avere più callback di requestIdleCallback? Sì, puoi avere più callback di requestAnimationFrame. Tuttavia, è bene ricordare che se la prima chiamata esaurisce il tempo rimanente, non ci sarà più tempo per eventuali altre richiamate. Gli altri callback dovranno poi 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 perda il tempo.
  • Cosa succede se imposto un nuovo callback inattivo all'interno di un'altra? La nuova callback di inattività verrà pianificata per essere eseguita il prima possibile, a partire dal frame next (anziché da quello corrente).

Inattiva!

requestIdleCallback è un modo eccezionale per assicurarti di poter eseguire il codice, ma senza interferire con l'utente. È semplice da usare e molto flessibile. Tuttavia, è ancora l'inizio e le specifiche non sono del tutto sistemate, quindi qualsiasi tuo feedback è gradito.

Dai un'occhiata in Chrome Canary, dai un'occhiata ai tuoi progetti e facci sapere come te la cavi.