Rilevamento delle funzionalità di WebAssembly

Scopri come utilizzare le funzionalità più recenti di WebAssembly, supportando al contempo gli utenti su tutti i browser.

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly 1.0 è stato rilasciato quattro anni fa, ma lo sviluppo non si è fermato qui. Le nuove funzionalità vengono aggiunte tramite il processo di standardizzazione della proposta. Come accade in genere con le nuove funzionalità sul web, l'ordine e le tempistiche di implementazione possono variare notevolmente da un motore all'altro. Se vuoi utilizzare queste nuove funzionalità, devi assicurarti che nessuno dei tuoi utenti venga escluso. Questo articolo illustra un approccio per raggiungere questo obiettivo.

Alcune nuove funzionalità migliorano le dimensioni del codice aggiungendo nuove istruzioni per operazioni comuni, altre aggiungono potenti primitive di prestazioni e altre ancora migliorano l'esperienza degli sviluppatori e l'integrazione con il resto del web.

Puoi trovare l'elenco completo delle proposte e le relative fasi nel repo ufficiale o monitorare lo stato di implementazione nei motori nella pagina ufficiale della roadmap delle funzionalità.

Per assicurarti che gli utenti di tutti i browser possano utilizzare la tua applicazione, devi capire quali funzionalità vuoi utilizzare. Poi, suddividili in gruppi in base al supporto del browser. Quindi, compila la base di codice separatamente per ciascuno di questi gruppi. Infine, lato browser, devi rilevare le funzionalità supportate e caricare il bundle JavaScript e Wasm corrispondente.

Scelta e raggruppamento delle caratteristiche

Vediamo questi passaggi scegliendo un insieme di funzionalità arbitrario come esempio. Supponiamo di voler utilizzare SIMD, thread e gestione delle eccezioni nella mia libreria per motivi di dimensioni e prestazioni. Il supporto dei browser è il seguente:

Una tabella che mostra il supporto del browser per le funzionalità scelte.
Visualizza questa tabella delle funzionalità su webassembly.org/roadmap.

Per assicurarti che ogni utente riceva l'esperienza più ottimizzata, puoi suddividere i browser nelle seguenti coorti:

  • Browser basati su Chrome: sono supportati thread, SIMD e gestione delle eccezioni.
  • Firefox: Thread e SIMD sono supportati, ma non la gestione delle eccezioni.
  • Safari: i thread sono supportati, ma non SIMD e la gestione delle eccezioni.
  • Altri browser: presupponi solo il supporto WebAssembly di base.

Questa suddivisione è in base al supporto delle funzionalità nella versione più recente di ciascun browser. I browser moderni sono sempre attuali e si aggiornano automaticamente, quindi nella maggior parte dei casi devi preoccuparti solo dell'ultima release. Tuttavia, se includi WebAssembly di base come coorte di riserva, puoi comunque fornire un'applicazione funzionante anche per gli utenti con browser obsoleti.

Compilazione per diversi set di funzionalità

WebAssembly non dispone di un modo integrato per rilevare le funzionalità supportate in fase di runtime, pertanto tutte le istruzioni del modulo devono essere supportate sulla destinazione. Per questo motivo, devi compilare il codice sorgente in Wasm separatamente per ciascuno di questi diversi set di funzionalità.

Ogni toolchain e sistema di compilazione è diverso e dovrai consultare la documentazione del tuo compilatore per sapere come modificare queste funzionalità. Per semplicità, nell'esempio seguente utilizzerò una libreria C++ a un solo file e mostrerò come compilarla con Emscripten.

Utilizzerò SIMD tramite l'emulazione SSE2, i thread tramite il supporto della libreria Pthreads e sceglierò tra la gestione delle eccezioni Wasm e l'implementazione JavaScript di riserva:

# First bundle: threads + SIMD + Wasm exceptions
$ emcc main.cpp -o main.threads-simd-exceptions.mjs -pthread -msimd128 -msse2 -fwasm-exceptions
# Second bundle: threads + SIMD + JS exceptions fallback
$ emcc main.cpp -o main.threads-simd.mjs -pthread -msimd128 -msse2 -fexceptions
# Third bundle: threads + JS exception fallback
$ emcc main.cpp -o main.threads.mjs -pthread -fexceptions
# Fourth bundle: basic Wasm with JS exceptions fallback
$ emcc main.cpp -o main.basic.mjs -fexceptions

Il codice C++ stesso può utilizzare #ifdef __EMSCRIPTEN_PTHREADS__ e #ifdef __SSE2__ per scegliere in modo condizionale tra le implementazioni parallele (thread e SIMD) delle stesse funzioni e le implementazioni seriali al momento della compilazione. L'aspetto sarà simile al seguente:

void process_data(std::vector<int>& some_input) {
#ifdef __EMSCRIPTEN_PTHREADS__
#ifdef __SSE2__
  // …implementation using threads and SIMD for max speed
#else
  // …implementation using threads but not SIMD
#endif
#else
  // …fallback implementation for browsers without those features
#endif
}

La gestione delle eccezioni non richiede direttive #ifdef, perché può essere utilizzata nello stesso modo da C++ indipendentemente dall'implementazione di base scelta tramite i flag di compilazione.

Caricamento del bundle corretto

Una volta creati i bundle per tutte le coorti di caratteristiche, devi caricare quello corretto dall'applicazione JavaScript principale. Per farlo, devi prima rilevare quali funzionalità sono supportate nel browser corrente. Puoi farlo con la libreria wasm-feature-detect. Se la combini con l'importazione dinamica, puoi caricare il bundle più ottimizzato in qualsiasi browser:

import { simd, threads, exceptions } from 'https://unpkg.com/wasm-feature-detect?module';

let initModule;
if (await threads()) {
  if (await simd()) {
    if (await exceptions()) {
      initModule = import('./main.threads-simd-exceptions.mjs');
    } else {
      initModule = import('./main.threads-simd.mjs');
    }
  } else {
    initModule = import('./main.threads.mjs');
  }
} else {
  initModule = import('./main.basic.mjs');
}

const Module = await initModule();
// now you can use `Module` Emscripten object like you normally would

Parole finali

In questo post ho mostrato come scegliere, creare e passare da un bundle all'altro per diversi set di funzionalità.

Man mano che il numero di funzionalità aumenta, il numero di coorti di funzionalità potrebbe non essere più gestibile. Per ovviare a questo problema, puoi scegliere le coorti di funzionalità in base ai dati utente reali, saltare i browser meno popolari e lasciare che tornino a coorti leggermente meno ottimali. Finché l'applicazione continua a funzionare per tutti gli utenti, questo approccio può fornire un equilibrio ragionevole tra il miglioramento progressivo e le prestazioni di runtime.

In futuro, WebAssembly potrebbe avere un modo integrato per rilevare le funzionalità supportate e passare tra diverse implementazioni della stessa funzione all'interno del modulo. Tuttavia, un tale meccanismo sarebbe già una funzionalità post-MVP che dovrai rilevare e caricare in modo condizionale utilizzando l'approccio sopra indicato. Fino ad allora, questo approccio rimane l'unico modo per compilare e caricare codice utilizzando le nuove funzionalità di WebAssembly su tutti i browser.