Come utilizzare la Background Tasks API in Javascript

In Javascript è spesso difficile capire quando schedulare ed eseguire operazioni con bassa priorità efficientemente. Questa API può aiutarti nell'intento.

Quando un browser carica una pagina web, ogni script al suo interno viene eseguito su un unico thread secondo il modello Event Loop. Ciò implica fondamentalmente che tutte le funzioni chiamate o invocate vengono messe in coda per poi essere eseguite dal motore Javascript, una alla volta. Per questo motivo più il tempo di esecuzione di una funzione sarà maggiore tanto più lo sarà il ritardo con cui le successive saranno eseguite con conseguente rischio di intaccare la velocità e la reattività della nostra pagina web.

Prendiamo ad esempio un sito web con una galleria di immagini. Quando l'utente scrolla la pagina possono essere disposte e messe in coda una serie di funzioni: una richiesta http per richiedere i dati delle immagini da visualizzare, l'inserimento nel DOM di nuovi elementi, l'invio di statistiche d'uso o di log ecc. Più l'utente effettua azioni sul nostro sito in breve tempo più la coda è destinata a crescere rapidamente. Ovviamente il motore Javascript non sa che priorità ha ciascuna funzione e perciò le mette in coda secondo l'ordine con cui sono state chiamate col rischio di non dare la precedenza a quelle con una priorità maggiore (ad esempio l'aggiornamento dell'interfaccia utente). Se poi si considerano scenari come la navigazione su dispositivi mobile, ecco che l'ordine e il tempo di esecuzione di ciascuna operazione acquistano valore ancora più sensibilmente.

La Background Tasks API (conosciuta anche come Cooperative Scheduling of Background Tasks API dal nome datole dal W3C o requestIdleCallback() dal nome della funzione) ci offre un valido aiuto nell'intento, infatti permette di schedulare operazioni da eseguire quando l'applicazione è in stato di idle. In questo modo possiamo far eseguire prima le operazioni con alta priorità e, quando queste sono state concluse, eseguire quelle con bassa priorità.

Caso d'uso

Prendiamo come esempio una generica funzione di log e usiamola al termine di una data operazione in questo modo

function log (action) {
    console.log(action + ": done!");
    // Fai qualcosa come inviare statistiche di utlizzo...
};

function doSomething (action) {
    console.log(action + ": executing...");
    // Fai qualcosa...
    log(action);
};

doSomething("operation #1");
doSomething("operation #2");
console.log("another operation");
doSomething("operation #3");

L'output sarà il seguente:

operation #1: executing...
operation #1: done!
operation #2: executing...
operation #2: done!
another operation
operation #3: executing...
operation #3: done!

Come puoi vedere, se chiamamo più volte la funzione doSomething() mettiamo in coda ogni operazione al suo interno, compresa l'esecuzione di log(), nell'ordine in cui si presentano. Questo non è ottimale dal punto di vista delle perfomance perchè se all'interno di doSomething() ci sono funzioni che devono essere eseguite il prima possibile, come aggiornare l'interfaccia utente o effettuare delle animazioni, è importante che abbiano la precedenza sulle funzioni con priorità minore per non rendere la navigazione lenta o non fluida.

A prima vista il problema sembra facilmente risolvibile rimuovendo la chiamata a log() dal corpo della funzione e chiamandola altrove

doSomething("operation #1");
doSomething("operation #2");
doSomething("operation #3");
log("operation #1");
log("operation #2");
log("operation #3");

Questo approccio presenta però diversi lati negativi. Così facendo aumentiamo la verbosità del nostro codice (immagina se invece che stringhe le nostre funzioni avessero preso come parametro funzioni anonime) e perdiamo il contesto della funzione di cui intendiamo fornire il log.

Un workaround usato in situzioni simili è quello di sfruttare setTimeout() per eseguire una funzione dopo quelle che sono state già schedulate.

function log (action) {
    setTimeout(function () {
        console.log(action + ": done!");
        // Fai qualcosa come inviare statistiche di utilizzo...
    }, 1);
};

Purtroppo neanche così è possibile risolvere completamente il problema dal momento che le operazioni presenti in doSomething() potrebbero essere funzioni asincrone o callback di un evento. In questo caso infatti non sarebbe possibile prevedere quando la funzione sarà schedulata e quindi non potremo essere certi di eseguire la funzione di log dopo le operazioni con priorità maggiore.

Sintassi

La funzione requestIdleCallback() accetta due parametri:

  • Una callback da eseguire mentre la pagina è in idle. Ad essa viene passato come parametro un oggetto con:

    • La proprietà didTimeout che tramite un valore booleano indica se la funzione è stata eseguita perchè è finito il tempo impostato di timeout (vedi di seguito).
    • Il metodo timeRemaining() che restituisce il tempo rimamente alla fine dello stato di idle corrente (massimo 50ms per motivi di perfomance). Può essere usato per eseguire una serie di funzioni in background.
  • Un oggetto opzionale per definire delle impostazioni. Al momento quest'ultimo ha una sola proprietà configurabile, timeout, a cui è possibile assegnare un numero intero positivo per definire il tempo massimo (in millisecondi) entro cui eseguire la funzione.

La funzione restituisce un id tramite cui è possibile cancellare l'esecuzione della callback utilizzando cancelIdleCallback().

Ecco quindi un esempio completo delle funzioni appena descritte:

var idleCallbackId = requestIdleCallback(function (idleDeadline) {
    // Vero se la funzione è stata eseguita perchè è scaduto il timeout impostato.
    var didTimeout = idleDeadline.didTimeout;
    // Tempo rimamente alla fine dello stato di idle corrente.
    var timeRemaining = idleDeadline.timeRemaining();
}, {
    // Tempo massimo entro cui eseguire la funzione. Opzionale.
    timeout: 3000
});

// Annulla l'esecuzione della funzione di background sopra schedulata
cancelIdleCallback(idleCallbackId);

Utilizzo

Rifattorizziamo log() utilizzando requestIdleCallback():

function log (action) {
    requestIdleCallback(function () {
        console.log(action + ": done!");
        // Fai qualcosa come inviare statistiche di utilizzo...
    })
}

Se ora rieseguiamo le operazioni precedenti vedremo che log(), nonostante sia chiamata più volte e prima di altre funzioni, non verrà eseguita prima di esse.

doSomething("operation #1");
doSomething("operation #2");
console.log("another operation");
doSomething("operation #3");
operation #1: executing...
operation #2: executing...
another operation
operation #3: executing...
operation #1: done!
operation #2: done!
operation #3: done!

Design pattern

  • Attenzione ad usare questa API per modificare il DOM. Ricordiamoci che requestIdleCallback() è pensata per effettuare task di minore importanza quando la pagina web è in stato di idle, quindi non è la maniera più veloce per aggiornare l'interfaccia utente. Inoltre la manipolazione del DOM può comportare un ricalcolo del layout degli elementi nella pagina impattando sulle perfomance. È quindi preferibile utilizzare un oggetto DocumentFragment, attaccarvi i nodi che intendiamo inserire nella nostra pagina e aggiungerlo al DOM utilizzando una funzione da eseguire fuori dallo stato di idle magari servendosi anche di un'API come requestAnimationFrame(). Vediamo come aggiungere un tag h1 alla nostra pagina seguendo questo approccio.
function updatePageTitle (titleElement) {
    if (requestAnimationFrame) {
        requestAnimationFrame(function () {
            document.body.insertBefore(titleElement, document.body.firstChild);
        });
    } else {
        document.body.insertBefore(titleElement, document.body.firstChild);
    }
};

requestIdleCallback(function () {
    var documentFragment = document.createDocumentFragment();
    var title = document.createElement("h1");
    title.textContent = "My title";
    documentFragment.appendChild(title);
    updatePageTitle(documentFragment);
});
  • Puoi usare requestIdleCallback() per eseguire una serie di funzioni in background. Per essere certi che allo scadere del tempo di idle non vengano tralasciate alcune, possiamo servirci del metodo timeRemaining() per rischedulare la callback fino ad eseguirle tutte.
// Funzioni da eseguire in background
var tasks = [
    function a () { console.log("function A") },
    function b () { console.log("function B") },
    function c () { console.log("function C") },
];

// Funzione principale per eseguire in background i nostri task
function executeTasks (idleDeadline) {
    // Finchè ci troviamo in stato di idle e ci sono ancora task in lista, eseguiamoli
    while (idleDeadline.timeRemaining() > 0 && tasks.length > 0) {
        // Esegui il primo task in lista e rimuovilo
        tasks.shift()();
    }
    // La pagina non è più in idle. Controlliamo se rimangono ancora task in lista e
    // in caso affermativo rischeduliamo questa funzione
    if (tasks.length > 0) {
        requestIdleCallback(executeTasks);
    }
};

requestIdleCallback(executeTasks);

Compatibilità

Nel momento in cui scrivo questo articolo, questa API è ancora in via di sviluppo e i browser che la supportano sono i seguenti:

Chrome Firefox Opera
47 55 34

Questo shim permette di usarla anche nei browser non supportati

window.requestIdleCallback = window.requestIdleCallback ||
  function (cb) {
    return setTimeout(function () {
      var start = Date.now();
      cb({ 
        didTimeout: false,
        timeRemaining: function () {
          return Math.max(0, 50 - (Date.now() - start));
        }
      });
    }, 1);
  }

window.cancelIdleCallback = window.cancelIdleCallback ||
  function (id) {
    clearTimeout(id);
} 

Fonte: gist.github.com/paullewis/55efe5d6f05434a96c36

Da notare l'uso di setTimeout() che abbiamo visto in precedenza per emulare quanto più possibile il comportamento dell'API.

Come creare un'applicazione Node.js con Docker, Parte 1: Deploy

Come creare un'applicazione Node.js con Docker, Parte 1: Deploy

In questo articolo vedremo come utilizzare Docker per eseguire un'applicazione Node.js senza più temere lunghe e difficoltose configurazioni.

Come creare un'applicazione Node.js con Docker, Parte 2: Sviluppo

Come creare un'applicazione Node.js con Docker, Parte 2: Sviluppo

Docker è un ottimo strumento non solo per il deploy delle applicazioni ma anche per tutte le altri fasi del ciclo di vita di un software. In questa seconda parte vedremo il suo utilizzo nello sviluppo della nostra applicazione web Node.js.

Come creare un'applicazione Node.js con Docker, Parte 3: Debugging

Come creare un'applicazione Node.js con Docker, Parte 3: Debugging

Quando si sviluppano applicazioni complesse il debugging è fondamentale per implementare nuove funzionalità, ottimizzare il codice e risolvere bug. Vediamo come usare gli strumenti messi a disposizione da Docker e da Node.js per affrontare al meglio questa importante attività.