Détection des fonctionnalités WebAssembly

Découvrez comment utiliser les dernières fonctionnalités WebAssembly tout en prenant en charge les utilisateurs dans tous les navigateurs.

WebAssembly 1.0 a été publié il y a quatre ans, mais le développement ne s'est pas arrêté là. Les nouvelles fonctionnalités sont ajoutées via le processus de standardisation des propositions. Comme c'est généralement le cas pour les nouvelles fonctionnalités sur le Web, leur ordre d'implémentation et leurs délais peuvent varier considérablement d'un moteur à l'autre. Si vous souhaitez utiliser ces nouvelles fonctionnalités, vous devez vous assurer qu'aucun de vos utilisateurs n'est exclu. Cet article vous explique comment procéder.

Certaines nouvelles fonctionnalités améliorent la taille du code en ajoutant de nouvelles instructions pour les opérations courantes, d'autres ajoutent des primitives de performances puissantes, et d'autres encore améliorent l'expérience des développeurs et l'intégration avec le reste du Web.

Vous trouverez la liste complète des propositions et de leurs étapes respectives dans le dépôt officiel ou pourrez suivre leur état d'implémentation dans les moteurs sur la page officielle de la feuille de route des fonctionnalités.

Pour vous assurer que les utilisateurs de tous les navigateurs peuvent utiliser votre application, vous devez déterminer les fonctionnalités que vous souhaitez utiliser. Divisez-les ensuite en groupes en fonction de la compatibilité avec les navigateurs. Compilez ensuite votre codebase séparément pour chacun de ces groupes. Enfin, côté navigateur, vous devez détecter les fonctionnalités compatibles et charger le bundle JavaScript et Wasm correspondant.

Sélectionner et regrouper des éléments géographiques

Voyons comment procéder en choisissant un ensemble d'éléments arbitraires comme exemple. Supposons que j'ai décidé d'utiliser SIMD, les threads et la gestion des exceptions dans ma bibliothèque pour des raisons de taille et de performances. La compatibilité avec les navigateurs est la suivante:

Tableau indiquant la compatibilité des navigateurs avec les fonctionnalités choisies.
Affichez ce tableau des fonctionnalités sur webassembly.org/roadmap.

Vous pouvez répartir les navigateurs dans les cohortes suivantes pour vous assurer que chaque utilisateur bénéficie de l'expérience la plus optimisée possible:

  • Navigateurs basés sur Chrome: les threads, SIMD et la gestion des exceptions sont tous compatibles.
  • Firefox: les threads et SIMD sont compatibles, mais pas le traitement des exceptions.
  • Safari: les threads sont compatibles, mais pas SIMD ni le traitement des exceptions.
  • Autres navigateurs: ne supposez que la compatibilité de base avec WebAssembly.

Cette répartition est basée sur la compatibilité des fonctionnalités dans la dernière version de chaque navigateur. Les navigateurs modernes sont toujours à jour et se mettent à jour automatiquement. Dans la plupart des cas, vous n'avez donc qu'à vous soucier de la dernière version. Toutefois, tant que vous incluez WebAssembly de référence comme cohorte de remplacement, vous pouvez toujours fournir une application fonctionnelle, même pour les utilisateurs disposant de navigateurs obsolètes.

Compilation pour différents ensembles de fonctionnalités

WebAssembly ne dispose pas d'un moyen intégré de détecter les fonctionnalités compatibles au moment de l'exécution. Par conséquent, toutes les instructions du module doivent être compatibles avec la cible. Par conséquent, vous devez compiler le code source en Wasm séparément pour chacun de ces différents ensembles de fonctionnalités.

Chaque chaîne d'outils et système de compilation est différent. Vous devrez consulter la documentation de votre propre compilateur pour savoir comment ajuster ces fonctionnalités. Par souci de simplicité, j'utiliserai une bibliothèque C++ au format fichier unique dans l'exemple suivant et je vous montrerai comment la compiler avec Emscripten.

J'utiliserai SIMD via l'émulation SSE2, les threads via la prise en charge de la bibliothèque Pthreads et je choisirai entre la gestion des exceptions Wasm et l'implémentation JavaScript de remplacement:

# 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

Le code C++ lui-même peut utiliser #ifdef __EMSCRIPTEN_PTHREADS__ et #ifdef __SSE2__ pour choisir de manière conditionnelle entre les implémentations parallèles (threads et SIMD) des mêmes fonctions et les implémentations séquentielles au moment de la compilation. Elle se présente comme suit:

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 gestion des exceptions n'a pas besoin de directives #ifdef, car elle peut être utilisée de la même manière à partir de C++, quelle que soit l'implémentation sous-jacente choisie via les indicateurs de compilation.

Charger le bon bundle

Une fois que vous avez créé des bundles pour toutes les cohortes de fonctionnalités, vous devez charger le bon bundle à partir de l'application JavaScript principale. Pour ce faire, commencez par détecter les fonctionnalités compatibles avec le navigateur actuel. Pour ce faire, utilisez la bibliothèque wasm-feature-detect. En l'associant à l'importation dynamique, vous pouvez charger le bundle le plus optimisé dans n'importe quel navigateur:

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

Dernières remarques

Dans cet article, je vous ai montré comment choisir, créer et basculer entre des bundles pour différents ensembles de fonctionnalités.

À mesure que le nombre de fonctionnalités augmente,le nombre de cohortes de fonctionnalités peut devenir impossible à gérer. Pour atténuer ce problème, vous pouvez choisir des cohortes de fonctionnalités en fonction de vos données utilisateur réelles, ignorer les navigateurs les moins populaires et les laisser revenir à des cohortes légèrement moins optimales. Tant que votre application fonctionne toujours pour tous les utilisateurs, cette approche peut fournir un équilibre raisonnable entre l'amélioration progressive et les performances d'exécution.

À l'avenir, WebAssembly pourrait disposer d'un moyen intégré de détecter les fonctionnalités compatibles et de basculer entre différentes implémentations de la même fonction dans le module. Toutefois, un tel mécanisme serait lui-même une fonctionnalité post-MVP que vous devrez détecter et charger de manière conditionnelle à l'aide de l'approche ci-dessus. En attendant, cette approche reste le seul moyen de compiler et de charger du code à l'aide des nouvelles fonctionnalités WebAssembly dans tous les navigateurs.