Javascript vs WebAssembly

Javascript (JS) è sicuramente il linguaggio più diffuso per lo sviluppo di applicazioni web, grazie anche alla sua versatilità e al grande numero di plugin che consentono una vasta serie di personalizzazioni. Esiste però anche un’alternativa, sviluppata dal W3C in collaborazione con i principali produttori di browser web, ovvero WebAssembly. Questo standard ha come obiettivo principale quello di abilitare applicazioni ad alte prestazioni sul Web e di rendere l’esecuzione sul browser efficiente e compatta.
Prima di parlare delle differenze tra i due, vediamo nel dettaglio come funziona JS e come si è arrivati alla definizione del WebAssembly.

JavaScript: dietro le quinte

JavaScript è un linguaggio di scripting; oltre che per i Siti Web, viene impiegato per realizzare Web App anche tramite l’ausilio di vari framework di sviluppo (NodeJS, Angular, ec..). Tramite JS è possibile ottenere degli effetti dinamici interattivi, grazie a funzioni invocate da eventi innescati dall’utente. Come riesce il browser a “interpretare” questo linguaggio? Grazie ad un “motore(Engine) in grado di eseguire il codice JS.

I primi motori JavaScript erano semplici interpreti, ovvero dei programmi che traducevano il linguaggio JS in codice eseguibile dal browser.
ECMAScript (ES) è la specifica standardizzata di JavaScript, di conseguenza i motori JS rispettano un certo grado di conformità alle specifiche definite dal ES. Più avanti in questo articolo vedremo esempi di motori JS con il loro relativo grado di conformità allo standard.

I motori JS sono in genere sviluppati dai fornitori di browser Web e ogni browser principale ne ha uno; ad esempio, il motore Chrome V8, motore di Google Chrome (che è anche un componente principale del popolare sistema di runtime Node.js), mentre Mozilla Firefox utilizza il motore open-source SpiderMonkey, Microsoft Edge utilizza il motore Chakra/ChackraCore e Apple ha sviluppato il motore WebKit, composto da JavaScriptCore e WebCore, che viene utilizzato sia su Safari che su tutti i browser iOS. Esiste anche un motore utilizzato per l’IoT: Moddable XS.

Da sinistra a destra: Chrome V8, SpiderMonkey, WebKit

Interprete e compilatore

In un browser, il motore JS viene eseguito insieme al motore di rendering. Oggi tutti i motori usano una combinazione di un interprete e di uno o più compilatori per migliorare le prestazioni. L’interprete esegue il codice JS quasi immediatamente, dopo averlo tradotto in codice eseguibile; il compilatore si occupa della generazione di codice eseguibile ottimizzato.

Sia la compilazione che l’ottimizzazione si traducono in un’esecuzione più rapida del codice, nonostante il tempo aggiuntivo necessario nella fase di compilazione. L’idea principale dietro ai motori moderni è quella di combinare il meglio dei due mondi:

  • Avvio rapido dell’applicazione dell’interprete.
  • Esecuzione rapida del compilatore.

Parallelamente all’esecuzione dell’interprete, il motore contrassegna le parti di codice eseguite frequentemente e le passa al compilatore insieme alle informazioni contestuali raccolte durante l’esecuzione. Questo processo consente al compilatore di adattare e ottimizzare il codice per il contesto corrente. Il comportamento di questi compilatori è chiamato “Just in Time” o semplicemente JIT. La maggior parte del lavoro del motore è quindi investita nelle operazioni di ottimizzazione.
Di seguito una sintetizzazione delle fasi eseguite da un tipico motore JS:

  1. Caricamento del codice sorgente, ovvero lo script JS
  2. L’Interprete fa partire l’applicazione (traducendo codice JS in codice eseguibile)
  3. Il Compilatore JIT:
    1. riceve le parti di codice eseguite di frequente
    2. inizia l’ottimizzazione e la compilazione, creando codice eseguibile ottimizzato.
    3. continua incrementalmente ad ottimizzare.

Di seguito un esempio della struttura del motore di Google Chrome, Chrome V8:

Nella prima fase Ignition (Interprete) viene caricato il codice JS per poter create una versione intermedia del codice, il bytecode, che diventerà poi eseguibile nella seconda fase. Durante l’Ignition viene effettuata una fase di parsing del codice e determinata la struttura sintattica di come è stato scritto. In questo modo l’interprete può “capire” il programma e convertirlo in bytecode. Il bytecode viene usato nella fase successiva Full-codegen per creare codice eseguibile non ottimizzato. In parallelo vengono avviati i 2 compilatori JIT per l’ottimizzazione: Crankshaft e TurboFan. In questo modo si crea il codice ottimizzato per l’architettura su cui verrà eseguito il codice, che andrà a sostituire quello non ottimizzato.
Oltre all’ottimizzazione, viene gestita anche la de-ottimizzazione.

Essendo JS un linguaggio dinamico è possibile definire variabili non tipate che quindi sono valorizzate a run-time; il compilatore quindi potrebbe ottimizzare la porzione di codice per gestire gli interi, ma durante l’esecuzione potrebbero essere utilizzate le stringhe. Per ovviare a questo problema, è stata aggiunta una funzionalità di de-ottimizzazione, ovvero di rimozione di “vecchie” ottimizzazioni per sostituirle. Questo processo fa comunque parte dell’ottimizzazione incrementale descritta in precedenza (Fase 5).

Di seguito un altro esempio: la struttura del motore SpiderMonkey di Mozilla Firefox:

Come si vede dall’immagine anche SpiderMonkey utilizza una strutturazione simile a quella di Chrome V8.

Memoria e performance dei motori JS

Anche solo confrontando questi due esempi di motori, notiamo che ad alto livello architetturale non ci sono particolari differenze.
Per quanto riguarda la memoria invece?
Per poter rispondere serve descrivere come viene gestita la memoria dai principali motori JS. Tutti i principali motori implementano questo in modo molto simile, utilizzando la specifica ECMAScript che definisce essenzialmente tutti gli oggetti come dizionari, con le chiavi di stringa associate agli attributi delle proprietà.

Definizione di tipi/oggetti ECMAScript

Visto come vengono definiti gli oggetti in JS, analizziamo in che modo i motori JS consentono di lavorare con gli oggetti in modo efficiente.
Nei programmi JS utilizzati comunemente, accedere alle proprietà è di gran lunga l’operazione più comune. È fondamentale quindi per i motori JS velocizzare l’accesso alle proprietà. In generale è comune avere più oggetti con le stesse chiavi di proprietà; avendo tali oggetti con forma comune è possibile ottimizzarne l’accesso alle proprietà in base alla loro forma.


Separando la forma dai valori è possibile creare una struttura comune “Shape”, mentre i valori rimangono persistiti in JSObject (JSO). Questa forma contiene tutti i nomi delle proprietà e gli attributi, ad eccezione dei loro valori. Invece “Shape” contiene l’offset dei valori all’interno di JSO, in modo che il motore JavaScript sappia dove trovare i valori. Ogni oggetto JSO con la stessa forma punta esattamente a questa istanza Shape. Ora ogni oggetto JSO deve solo memorizzare i valori che sono unici per questo oggetto.
È quindi sull’ottimizzazione in memoria ed uso in chache degli shape che il motore può migliorare le sue prestazioni. I vari motori nominati in precedenza definiscono gli shape con nomenclature diverse:

  • ChromeV8 – Maps
  • SpiderMonkey – Shapes
  • WebKit (JavaScriptCore) – Structures
  • Microsoft Chakra – Types

Differenze tra motori

Nonostante le diverse nomenclature degli Shape, anche per gestire la memoria non si notano grosse differenze. Esiste però un identificatore che mostra bene le differenze tra i vari motori ed è il grado di conformità verso lo standard ECMAScript. Questo grado o percentuale di conformità è valorizzato da un test, che consiste in migliaia di test individuali (creati da ECMA International) che verificano i requisiti della specifica e viene utilizzato per verificare se l’implementazione segue gli standard di specifica ECMAScript.
Ad oggi i risultati del test, per i motori citati in precedenza, sono:

  • Microsoft ChackraCore – 65%
  • JavascriptCore (WebKit) – 82%
  • SpiderMonkey – 88%
  • Chrome V8 – 97%
  • Moddable XS – 94%

Il test è consultabile al seguente URL: https://test262.report/

Cosa cambia con WebAssembly

Con l’introduzione dello standard WebAssembly (WASM), viene introdotta la possibilità di creare codice binario eseguibile dal motore del browser, partendo da linguaggi diversi (C++, C#, F# Python, Java, Go, ed anche Javascript stesso).
L’obiettivo principale di WebAssembly è abilitare applicazioni ad alte prestazioni su pagine Web, ma il formato è progettato per essere eseguito e integrato anche in altri ambienti. Ad oggi tutti i principali motori dei browser hanno implementato la possibilità di eseguire codice WASM.
Di seguito una pagina dove è possibile vedere nel dettaglio le versioni dei browser che supportano WASM
: https://caniuse.com/#search=web%20assembly
Con WASM quindi viene superata la fase di interpretazione del codice (Fase 2) poiché il codice della pagina web arriverà dal server già compilato ed eseguibile dal browser. WASM supporta il multithreading e garbage collection migliorandone così le prestazioni di esecuzione.
Per generare codice eseguibile WASM a partire da linguaggi sorgente eterogenei è necessario utilizzare uno specifico compilatore. Per il C/C++ viene utilizzato l’SDK Emscripten, mentre per esempio per C# è possibile utilizzare WASM attraverso Blazor.

JavaScript vs WebAssembly

Vediamo infine quali sono i pro e contro di queste due tipologie:
Sicuramente JS ha a disposizione un’ampia varietà di funzionalità già pronte e framework che WASM non ha (anche se è possibile implementarle da zero). La stessa cosa vale per il supporto dei vari framework; essendo JS utilizzato da molto più tempo le documentazioni e le risoluzioni dei problemi sono più facili da reperire per JS.
Per progetti su larga scala, con l’utilizzo di una grande quantità di framework JS le dimensioni delle pagine posso crescere significativamente. Su questo fronte WASM fornisce file di dimensioni contenute allo scaricamento della pagina web, anche se per progetti più piccoli JS potrebbe ancora essere minore come dimensioni.

I file di WASM vengono caricati nella cache del browser più velocemente del corrispondente codice JavaScript; se si utilizza spesso la stessa applicazione, o se la Web App si trova in locale, il binario WASM verrà avviato più rapidamente.
Altro fattore da non trascurare è il supporto da parte dei browser Web, non tutti i browser infatti offrono le medesime prestazioni con WASM. Questo è un fattore da tenere in considerazione quando si sceglie di sviluppare un’applicazione indipendente dalla piattaforma o dal browser.
WASM consente di poter sviluppare codice con il linguaggio che si preferisce, adattandosi quindi alle esigenze degli sviluppatori che non avranno bisogno di imparare nuovi linguaggi.

Con WASM è possibile sviluppare applicazioni web che necessitano di alte prestazioni come per esempio videogiochi, realtà virtuale ecc.
Per quanto riguarda la sicurezza, anche WASM non è esente da problemi, così come JS (vedi le Cross-site vulnerabilities). WASM è stato criticato poiché consentirebbe una maggiore facilità nel nascondere malware e attacchi di phishing, essendo WASM presente sul computer dell’utente solo nella sua forma compilata.

Conclusione

In conclusione, quindi, nonostante le migliori prestazioni e la facilità di sviluppo di WebAssembly, forse JavaScript rimane ancora la soluzione di maggior successo. WASM potrebbe avere la possibilità di sostituire JS vista la capacità di scrivere codice sorgente ottimizzato e “multi-linguaggio”; vedremo se nei prossimi anni le cose cambieranno, grazie anche ad un sempre maggiore supporto di questo – relativamente nuovo – standard.

Riferimenti e link utili:

https://v8.dev/
https://developer.mozilla.org/it/docs/SpiderMonkey
https://webkit.org/
https://test262.report/about
https://developer.mozilla.org/en-US/docs/WebAssembly
https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor
https://emscripten.org/docs/compiling/WebAssembly.html
Designed by macrovector / Freepik