Détection des fonctionnalités WebAssembly

Découvrez comment utiliser les dernières fonctionnalités WebAssembly tout en répondant aux besoins des utilisateurs sur tous les navigateurs.

WebAssembly 1.0 est sorti il y a quatre ans, mais le développement ne s'est pas arrêté là. De nouvelles fonctionnalités sont ajoutées via le processus de normalisation des propositions. Comme c'est généralement le cas pour les nouvelles fonctionnalités sur le Web, l'ordre d'implémentation et les délais peuvent varier considérablement d'un moteur à l'autre. Si vous souhaitez profiter de ces nouvelles fonctionnalités, vous devez vous assurer qu'aucun de vos utilisateurs n'est laissé de côté. Dans cet article, vous découvrirez la méthode à suivre pour y parvenir.

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

Vous trouverez la liste complète des propositions et leurs étapes respectives dans le dépôt officiel ou vous pouvez 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 accéder à votre application, vous devez identifier les fonctionnalités que vous souhaitez utiliser. Divisez-les ensuite en groupes en fonction de la compatibilité des navigateurs. Compilez ensuite votre codebase séparément pour chacun de ces groupes. Enfin, du côté du navigateur, vous devez détecter les fonctionnalités compatibles et charger le bundle JavaScript et Wasm correspondant.

Choix et regroupement des caractéristiques

Prenons l'exemple d'un ensemble de caractéristiques arbitraire. Supposons que je souhaite utiliser SIMD, des threads et la gestion des exceptions dans ma bibliothèque pour des raisons de taille et de performances. Leurs navigateurs compatibles sont les suivants:

Tableau indiquant la compatibilité des navigateurs avec les fonctionnalités sélectionnées.
Consultez ce tableau des fonctionnalités sur webassembly.org/roadmap.

Pour que chaque utilisateur bénéficie de la meilleure expérience utilisateur possible, vous pouvez diviser les navigateurs en cohortes suivantes:

  • Navigateurs Chrome: Threads, SIMD et la gestion des exceptions sont tous compatibles.
  • Firefox: Thread et SIMD sont compatibles, mais pas la gestion des exceptions.
  • Safari: les threads sont pris en charge, mais pas les cartes SIMD ni la gestion des exceptions.
  • Autres navigateurs: supposons uniquement que la compatibilité de base avec WebAssembly soit assurée.

Ces données sont réparties par fonctionnalité compatible dans la dernière version de chaque navigateur. Les navigateurs récents sont conçus pour fonctionner de manière permanente et se mettent à jour automatiquement. Par conséquent, dans la plupart des cas, vous n'avez à vous soucier que de la dernière version. Toutefois, tant que vous incluez WebAssembly de référence en tant que cohorte de remplacement, vous pouvez toujours fournir une application fonctionnelle, même pour les utilisateurs disposant de navigateurs obsolètes.

Compiler pour différents ensembles de caractéristiques

WebAssembly ne dispose pas d'un moyen intégré pour détecter les fonctionnalités compatibles lors de l'exécution. Par conséquent, toutes les instructions du module doivent être prises en charge sur la cible. C'est pourquoi vous devez compiler le code source dans Wasm séparément pour chacun de ces différents ensembles de caractéristiques.

Chaque chaîne d'outils et chaque système de compilation sont différents, et vous devrez consulter la documentation de votre propre compilateur pour savoir comment ajuster ces fonctionnalités. Par souci de simplicité, je vais utiliser une bibliothèque C++ à fichier unique dans l'exemple suivant et montrer comment la compiler avec Emscripten.

J'utilise SIMD via l'émulation SSE2, des threads via la compatibilité avec la bibliothèque Pthreads, et je choisis le traitement 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) de mêmes fonctions et les implémentations série 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
}

Le traitement des exceptions ne nécessite pas de directives #ifdef, car il peut être utilisé de la même manière à partir de C++, quelle que soit l'implémentation sous-jacente choisie via les indicateurs de compilation.

Chargement du bundle approprié

Une fois que vous avez créé des bundles pour toutes les cohortes de fonctionnalités, vous devez charger le bon depuis l'application JavaScript principale. Pour ce faire, commencez par identifier 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

Derniers mots

Dans cet article, je vous ai montré comment choisir, créer et passer d'un lot à l'autre pour différents ensembles de fonctionnalités.

À mesure que le nombre de caractéristiques augmente,le nombre de cohortes de caractéristiques peut devenir impossible à gérer. Pour atténuer ce problème, vous pouvez choisir des cohortes de fonctionnalités basées sur vos données d'utilisateurs réelles, ignorer les navigateurs moins populaires et les laisser se rabattre sur des cohortes légèrement moins optimales. Tant que votre application fonctionne pour tous les utilisateurs, cette approche peut fournir un équilibre raisonnable entre amélioration progressive et 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. Cependant, un tel mécanisme serait en soi une fonctionnalité post-MVP que vous devrez détecter et charger de manière conditionnelle en utilisant 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.