Costruire applicazioni e siti web performanti, reattivi e costantemente aggiornati in tempo reale richiede una grossa capacità e una vasta conoscenza delle tecnologie moderne.

HTML5, il nuovo standard del web, è stato rilasciato per la prima volta nell'Ottobre del 2014, portando con sé una serie di interessanti novità.

I browser hanno alimentato a pieno ritmo gli sviluppi per supportare le nuove funzionalità; in testa a tutti, Chrome e Firefox.

 

Quali sono dunque le novità introdotte?

Oltre ad un nuovo set di elementi e attributi semantici atti a sostituire i troppo abusati <div> e <span>, HTML5 introduce un entusiasmante nuovo mondo di API usufruibile via Javascript:

 

  • • WebSocket e Server Socket Event (SSE): tecnologie che permettono di realizzare applicazioni real-time
  • • WebWorker, Shared Worker e Service Worker: queste tecnologie permettono allo sviluppatore di lavorare con i thread, trasformando le applicazioni single-thread di Javascript in vere e proprie applicazioni multi-thread

  • • Geolocation: permette di recuperare informazioni sulla posizione geografica del client

  • • Push Notifications: notifiche cross-tab messe a disposizione dal browser

  • • IndexedDB: possibilità di avere storaging di grosse quantità di dati anche sul client

  • • Fetch: la nuova API in sostituzione dell'infernale XMLHttpRequest per eseguire chiamate AJAX

  • • FileSystem: incredibile novità per leggere e scrivere file in una sandbox del browser

 

E la lista continua con altre più o meno importanti feature.

 

L'argomento trattato in questa sede sono i WebWorker, ossia come rendere performanti le applicazioni web trasformandole da single-thread a multi-thread.

Prima di iniziare però è necessario fare sempre fede al sito caniuse  per conoscere la compatibilità e il supporto dato dai diversi browser alla tecnologia che vogliamo utilizzare: ad esempio i WebWorker sono supportati dalla quasi totalità dei browser.

 

WebWorker

I WebWorker, o più semplicemente Worker, sono oggetti che eseguono codice Javascript in un thread separato da quello principale in cui vengono istanziati.

I worker sono eseguiti in un contesto separato, con oggetti globali propri e altri condivisi con il thread principale.

Lo scope globale di un worker è espresso tramite l'oggetto DedicatedWorkerGlobalScope, in cui sono presenti gli oggetti globali a disposizione dell'utente, quali console, navigator, postMessage, onmessage e via dicendo.

È possibile accedere a un worker solamente dallo script in cui è stato dichiarato mentre se si vuole accederci da script differenti è necessario usare gli SharedWorker.

 

Features

I worker danno la possibilità di eseguire codice arbitrario in un thread separato ma con qualche eccezione, una su tutte la possibilità di manipolare il DOM per motivi di sicurezza (thread-safe).

Ecco una lista delle funzionalità:

 

  • • accedere all'oggetto navigator

  • • fare chiamate AJAX tramite l'oggetto XMLHttpRequest o il più recente ma meno supportato Fetch

  • • stabilire connessioni permanenti con i WebSocket

  • • salvare i dati localmente tramite IndexedDB

  • • accedere alla cache

  • • comunicare con il thread principale (e con altri worker nel caso degli SharedWorker)

  • • utilizzare le Push Notifications

  • • istanziare altri worker (normali WebWorker ma anche SharedWorker e ServiceWorker)

 

Grazie a tali funzionalità, vediamo come sia possibile spostare certe logiche su un thread separato, lasciando a quello principale il compito di renderizzare e manipolare il DOM, aumentando così l'efficienza e la velocità dell'applicazione.

 

Per fare un esempio pratico: dovete scrivere un'applicazione che, tramite i WebSocket, aggiorni i dati locali salvati grazie a IndexedDB e soltanto sotto certe condizioni deve notificare l'utente attraverso Push Notifications.

Verrà creato un worker che avrà l'incarico di stabilire la connessione con il backend e mantenere monitorati i dati in arrivo, salvandoli di volta in volta nel database locale; quando le condizioni sopracitate sono raggiunte, notificherà l'utente con l'apposita API ed eventualmente passerà al thread principale i dati richiesti.

In questo modo l'utente continuerà a lavorare comodamente con l'applicazione, senza accorgersi di eventuali blocchi causati dall'elaborazione dei dati in arrivo dal server.

 

Comunicazione

Come abbiamo già detto, i worker mantengono un canale di comunicazione col thread principale attraverso un set di API.

postMessage è la funzione con cui inviare dati dal worker verso il thread genitore e viceversa, mentre onmessage è l'evento per intercettare un nuovo messaggio.

 

Esempio (parrot):

 

// app.js

var worker = new Worker('worker.js');
worker.onmessage = function (event) {
    console.log(event.data); // pong in arrivo
    setTimeout(function () {
    worker.postMessage('ping'); // ping di risposta
    }, 2000);
};
worker.postMessage('ping'); // primo ping

// worker.js
onmessage = function (event) {
    // ping in arrivo. Verrà stampato nella console del browser
    console.log(event.data); 
    postMessage('pong');
};


 

L'esempio dimostra un semplice “parrot”, ossia viene inviato un messaggio “ping” ogni due secondi a cui riceve immediatamente in risposta un “pong”.

 

Oggetti trasferibili

Le stringhe non sono gli unici oggetti a poter essere trasferiti nel canale di comunicazione, bensì oggetti più complessi, come File, Blob, ArrayBuffer e JSON.

Di base, i dati passati con postMessage vengono copiati, creando inutile overhead; per evitare tale spreco di memoria e tempo, si utilizzano particolari oggetti con una firma differente della postMessage.

 

worker.postMessage(arrayBuffer, [arrayBuffer]);

 

In questo modo, i dati sono passati per riferimento con una sostanziale differenza: l'elemento passato dal mittente sarà completamente trasferito, quindi reperibile soltanto dal ricevente.

Per usare questi oggetti trasferibili, la postMessage richiede che il secondo parametro sia un ArrayBuffer.

 

Import

Può tornare molto utile caricare delle librerie all'interno di un worker: per farlo basta importarle con la funzione globale importScript.

 

Esempio:

 


// myAwesomeLib.js

function awesomeFunction() {
 console.log('hello world!');
}

// worker.js
importScript('myAwesomeLib.js');
awesomeFunction(); // scriverà “hello world” in console

 

Gestire gli errori

Nel caso venga sollevata un'eccezione all'interno di un WebWorker, è possibile intercettarla mettendosi in ascolto dell'evento 'error'.

 

Esempio:

 


// main.js

var worker = new Worker('worker.js');
worker.onerror = function (err) {
console.error(err);
};

 

L'oggetto di errore conterrà le informazioni inerenti al numero di riga (err.lineno), al nome del file (err.filename) e all'eventuale messaggio (err.message).

 

Sicurezza

Non essendoci ancora un meccanismo per personalizzare la sicurezza dei WebWorker, il browser non consente di caricarli da origini differenti; nuovi worker devono essere istanziati coi medesimi protocollo, host e porta del thread principale.

Per lo stesso motivo, ai WebWorker non è concesso l'accesso al DOM anche se rimangono accessibili le informazioni locali, come quelle presenti sugli IndexedDB ed altri worker.

 

Conclusione

I WebWorker sono ormai supportati dai principali browser, consentendo di scrivere applicazioni multithreading già da oggi.

Le API sono poche e semplici da utilizzare ma nel caso si preferisse un approccio differente da quello a eventi, ci sono alcune librerie che permettono l'uso di promise e callback, come catiline e parallel.

Una lista di casi d'uso reali che possono essere presi in consegna dai WebWorker:

 

  • • precaricamento e caching dei dati per un futuro utilizzo

  • • formattazione di dati in tempo reale

  • • manipolare stream audio/video

  • • operazioni di I/O che devono essere eseguite in brackground

  • • comunicazione col server tramite Ajax polling o websocket

  • • gestire grosse quantità di dati da e verso un database locale

  • • comprimere/espandere archivi

 

Risulta semplice rendere le proprie applicazioni web veloci e performanti, spostando calcoli lunghi e pesanti su thread separati e mantenendo il thread principale solo per aggiornare le viste.

Vincenzo Ferrari