Détection des fonctionnalités WebAssembly

Découvrez comment utiliser les dernières fonctionnalités WebAssembly tout en prenant en charge les 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 standardisation des propositions. Comme c'est généralement le cas pour les nouvelles fonctionnalités sur le Web, l'ordre et les délais d'implémentation peuvent varier considérablement d'un moteur à l'autre. Si vous souhaitez profiter de ces nouvelles fonctionnalités, assurez-vous qu'aucun de vos utilisateurs n'est laissé de côté. Cet article vous explique comment y parvenir.

Certaines nouvelles fonctionnalités améliorent la taille du code en ajoutant de nouvelles instructions pour les opérations courantes, d'autres ajoutent de puissantes primitives de performances, tandis que d'autres 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 leurs étapes respectives dans le dépôt officiel. Vous pouvez également suivre l'état de leur 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é du navigateur. Ensuite, compilez 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 caractéristiques

Passons en revue ces étapes en choisissant un ensemble de caractéristiques arbitraire comme exemple. Supposons que j'ai identifié que je souhaite utiliser le SIMD, les threads et la gestion des exceptions dans ma bibliothèque pour des raisons de taille et de performances. Leur compatibilité avec les navigateurs est la suivante:

<ph type="x-smartling-placeholder">
</ph> Tableau indiquant la compatibilité des navigateurs avec les fonctionnalités sélectionnées. <ph type="x-smartling-placeholder">
</ph> Consultez ce tableau des fonctionnalités sur webassembly.org/roadmap.

Vous pouvez répartir les navigateurs dans les cohortes suivantes afin d'optimiser l'expérience de chaque utilisateur:

  • Navigateurs basés sur Chrome: les threads, le SIMD et la gestion des exceptions sont tous compatibles.
  • Firefox: Thread et SIMD sont pris en charge, mais pas la gestion des exceptions.
  • Safari: les threads sont compatibles, mais pas le SIMD ni la gestion des exceptions.
  • Autres navigateurs: ils supposent uniquement la compatibilité de base de WebAssembly.

Cette répartition se fait par fonctionnalité de compatibilité dans la dernière version de chaque navigateur. Les navigateurs récents sont permanents 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 dont les navigateurs sont obsolètes.

Compiler pour différents ensembles de fonctionnalités

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

Chaque chaîne d'outils et chaque système de compilation sont différents. Vous devez donc consulter la documentation de votre propre compilateur pour savoir comment modifier ces fonctionnalités. Par souci de simplicité, nous allons 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 prise en charge de la bibliothèque Pthreads, et je choisis 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 en série au moment de la compilation. Il doit se présenter 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 d'instructions #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 options de compilation.

Charger le bon bundle

Une fois que vous avez créé des groupes pour toutes les cohortes de caractéristiques, vous devez charger la bonne à partir de l'application JavaScript principale. Pour cela, 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

Derniers mots

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

Plus le nombre de caractéristiques augmente,plus le nombre de cohortes de caractéristiques augmente. Pour atténuer ce problème, vous pouvez choisir des cohortes de caractéristiques basées sur vos données utilisateur réelles, ignorer les navigateurs moins populaires et les laisser utiliser des cohortes légèrement moins optimales. Tant que votre application fonctionne pour tous les utilisateurs, cette approche offre un équilibre raisonnable entre amélioration progressive et performances d'exécution.

À l'avenir, WebAssembly disposera peut-être 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 devriez détecter et charger de manière conditionnelle en utilisant l'approche ci-dessus. Jusqu'à cette date, cette approche reste le seul moyen de compiler et de charger du code à l'aide des nouvelles fonctionnalités WebAssembly dans tous les navigateurs.