Rilevamento delle funzionalità di WebAssembly

Scopri come utilizzare le ultime funzionalità di WebAssembly e supportare 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 in genere per le nuove funzionalità sul web, l'ordine e le tempistiche di implementazione possono variare significativamente tra i diversi motori. Se desideri utilizzare queste nuove funzionalità, devi assicurarti che nessuno dei tuoi utenti venga escluso. In questo articolo scoprirai un approccio per raggiungere questo obiettivo.

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

Puoi trovare l'elenco completo delle proposte e le rispettive fasi nel repository ufficiale oppure monitorare il loro stato di implementazione nei motori nella pagina ufficiale della roadmap delle funzionalità.

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

Funzionalità di selezione e raggruppamento

Esaminiamo questi passaggi scegliendo come esempio un insieme di caratteristiche arbitrarie. Supponiamo di aver scelto di utilizzare SIMD, thread e gestione delle eccezioni nella mia raccolta per motivi di dimensioni e prestazioni. Il supporto del browser è il seguente:

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

Puoi suddividere i browser nelle seguenti coorti per assicurarti che ogni utente usufruisca dell'esperienza più ottimizzata:

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

Questa analisi è suddivisa in base al supporto delle funzionalità nell'ultima versione di ciascun browser. I browser moderni sono evergreen e si aggiornano automaticamente, quindi nella maggior parte dei casi devi preoccuparti solo dell'ultima release. Tuttavia, purché includi WebAssembly di base come coorte di fallback, puoi comunque fornire un'applicazione funzionante anche per gli utenti con browser obsoleti.

Compilazione per diversi set di caratteristiche

WebAssembly non dispone di un modo integrato per rilevare le funzionalità supportate in runtime, pertanto tutte le istruzioni nel modulo devono essere supportate nella destinazione. Per questo motivo, dovrai compilare il codice sorgente in Wasm separatamente per ognuno dei diversi set di caratteristiche.

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

Utilizzerò SIMD tramite emulazione SSE2, thread tramite il supporto della libreria Pthreads e sceglierò tra Wasm Exception Management (Gestione delle eccezioni) 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 in fase di compilazione. L'URL avrà il seguente aspetto:

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 istruzioni #ifdef poiché può essere utilizzata allo stesso modo da C++ a prescindere dall'implementazione sottostante scelta tramite i flag di compilazione.

Caricamento del bundle corretto in corso...

Dopo aver creato i bundle per tutte le coorti di funzionalità, devi caricare quello corretto dall'applicazione JavaScript principale. Per farlo, devi innanzitutto rilevare le funzionalità supportate nel browser corrente. Puoi farlo con la libreria wasm-feature-detect. Combinandola 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

Ultime parole

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

Con l'aumento del numero di funzionalità,il numero di coorti di funzionalità potrebbe diventare non gestibile. Per alleviare il problema, puoi scegliere coorti di funzionalità in base ai dati utente reali, ignorare i browser meno popolari e consentire loro di passare a coorti leggermente meno ottimali. Finché l'applicazione continua a funzionare per tutti gli utenti, questo approccio può fornire un ragionevole equilibrio tra miglioramento progressivo e prestazioni di runtime.

In futuro, WebAssembly potrebbe offrire un modo integrato per rilevare le funzionalità supportate e passare tra le diverse implementazioni della stessa funzione all'interno del modulo. Tuttavia, un meccanismo di questo tipo sarebbe a sua volta una funzionalità post-MVP che dovresti rilevare e caricare in modo condizionale utilizzando l'approccio precedente. Fino ad allora, questo approccio rimane l'unico modo per creare e caricare codice utilizzando le nuove funzionalità di WebAssembly in tutti i browser.