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 deve intralciare l'utente. Se invii dati di analisi mentre 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 che può aiutarti: requestIdleCallback. Così come l'adozione di requestAnimationFrame ci ha permesso di pianificare correttamente le animazioni e massimizzare le probabilità di raggiungere i 60 fps, requestIdleCallback pianificherà il lavoro 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 provarlo subito utilizzando Chrome Canary. Si tratta di una funzionalità sperimentale e le specifiche sono ancora in fase di definizione, pertanto le cose potrebbero cambiare in futuro.

Perché dovrei utilizzare requestIdleCallback?

Pianificare autonomamente il lavoro non essenziale è molto difficile. È impossibile capire esattamente quanto tempo rimane per il frame perché dopo l'esecuzione di requestAnimationFrame callback sono necessari calcoli di stili, layout, pittura e altri componenti interni del browser. Una soluzione home-roll non può tenere conto di nessuno di questi. Per assicurarti che un utente non stia interagendo in qualche modo, devi anche associare 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 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 tramite 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.

Verifica di requestIdleCallback

requestIdleCallback è ancora agli inizi, quindi prima di utilizzarlo devi verificare che sia disponibile:

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

Puoi anche modificare il relativo 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 è ottimale perché non conosce il tempo di inattività come requestIdleCallback, ma poiché chiameresti direttamente la funzione se requestIdleCallback non fosse disponibile, non peggiori la situazione con questo tipo di shim. Con il shim, se requestIdleCallback è disponibile, le chiamate verranno reindirizzate in modo silenzioso, il che è fantastico.

Per il momento, però, supponiamo che esista.

Utilizzo di requestIdleCallback

La chiamata a requestIdleCallback è molto simile a quella di requestAnimationFrame in quanto accetta 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 pianificare un altro requestIdleCallback se hai ancora del 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 fai se le cose sono davvero impegnative? Potresti temere che la chiamata di ritorno non venga mai effettuata. Sebbene requestIdleCallback assomigli a requestAnimationFrame, differisce anche dal secondo parametro facoltativo, ovvero 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 didTimeout è True, molto probabilmente vorrai semplicemente eseguire il lavoro e terminare:

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. Se possibile, 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 vorremmo monitorare un evento come, ad esempio, il tocco di un menu di navigazione. Tuttavia, poiché normalmente vengono animate sullo schermo, è consigliabile evitare di inviare immediatamente questo evento a Google Analytics. Creeremo un array di eventi da inviare e ne richiederemo l'invio in un secondo 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 dobbiamo utilizzare requestIdleCallback per elaborare gli eventi in attesa:

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 dalla tua 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 eventuali interazioni e non causi scatti.

Utilizzo di requestIdleCallback per apportare modifiche al DOM

Un'altra situazione in cui requestIdleCallback può davvero migliorare il rendimento è quando devi apportare modifiche non essenziali al DOM, ad esempio aggiungere elementi alla fine di un elenco con caricamento differito in continua crescita. Vediamo come requestIdleCallback si inserisce in un frame tipico.

Un frame tipico.

È possibile che il browser sia troppo occupato per eseguire eventuali callback in un determinato frame, pertanto non dovresti aspettarti che alla fine di un frame ci sia qualsiasi tempo libero per eseguire altri lavori. Questo lo rende diverso da un'altra metrica come setImmediate, che viene eseguita 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 di stile saranno state applicate e, soprattutto, il layout sarà stato calcolato. Se apportiamo modifiche al DOM all'interno del callback di inattività, i calcoli del layout verranno invalidati. Se nel frame successivo sono presenti letture di layout di qualsiasi tipo, ad esempio getBoundingClientRect, clientWidth e così via, il browser dovrà eseguire un layout sincrono forzato, che rappresenta un potenziale collo di bottiglia per il rendimento.

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à poi essere aggiunto nel successivo callback requestAnimationFrame. Se utilizzi una libreria VDOM, dovrai usare requestIdleCallback per apportare modifiche, ma dovrai applicare le patch DOM nel successivo callback requestAnimationFrame, non nel callback di inattività.

Tenendo presente 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, viene chiamato scheduleVisualUpdateIfNeeded, che configura un singolo callback requestAnimationFrame che, a sua volta, aggiunge 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;
}

Se tutto va bene, ora vedremo molto meno scatti quando aggiungi elementi al DOM. Eccellente

Domande frequenti

  • È presente un polyfill? Purtroppo no, ma è presente uno shim se vuoi avere un reindirizzamento trasparente a setTimeout. Questa API esiste perché colma una lacuna molto reale nella piattaforma web. È difficile dedurre una mancanza di attività, ma non esistono API JavaScript per determinare la quantità di tempo libero alla fine del frame, quindi al massimo devi fare delle supposizioni. API come setTimeout, setInterval o setImmediate possono essere utilizzate per pianificare il lavoro, ma non sono programmate per evitare l'interazione dell'utente come avviene per requestIdleCallback.
  • Cosa succede se supero la scadenza? Se timeRemaining() restituisce zero, ma scegli di eseguire l'operazione per più tempo, puoi farlo senza temere che il browser interrompa il tuo lavoro. Tuttavia, il browser ti fornisce la scadenza per provare a garantire un'esperienza fluida per i tuoi utenti, quindi, a meno che non ci sia un motivo molto valido, devi sempre rispettare la scadenza.
  • Esiste un valore massimo restituito da timeRemaining()? Sì, attualmente dura 50 ms. Quando cerchi di mantenere un'applicazione reattiva, tutte le risposte alle interazioni degli utenti devono essere inferiori a 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ù chiamate di ritorno inattive programmate una dopo l'altra (se il browser determina che c'è tempo sufficiente per eseguirle).
  • Esiste un tipo di lavoro che non devo fare in requestIdleCallback? Idealmente, il lavoro che svolgi dovrebbe essere suddiviso in piccoli blocchi (microattività) con caratteristiche relativamente prevedibili. Ad esempio, la modifica del DOM in particolare avrà tempi di esecuzione imprevedibili, poiché attiverà i calcoli degli stili, il layout, la pittura e il 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, devi utilizzare il timeout.
  • Posso avere più callback requestIdleCallback? Sì, proprio come puoi avere più chiamate requestAnimationFrame. Tuttavia, è bene ricordare che se il primo riavvio utilizza tutto il tempo rimanente durante il riavvio, non rimarrà tempo per altri riavvii. Gli altri callback dovranno quindi attendere che il browser sia di nuovo inattivo prima di poter essere eseguiti. A seconda del lavoro che stai cercando di svolgere, potrebbe essere meglio avere un singolo callback inattivo e suddividere il lavoro al suo interno. In alternativa, puoi utilizzare il timeout per assicurarti che nessun callback venga perso per mancanza di tempo.
  • Cosa succede se impostiamo un nuovo callback inattivo 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).

Inattività attiva.

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.