Detección de funciones de WebAssembly

Aprende a usar las funciones más recientes de WebAssembly y admite usuarios en todos los navegadores.

WebAssembly 1.0 se lanzó hace cuatro años, pero el desarrollo no se detuvo allí. Las funciones nuevas se agregan mediante el proceso de estandarización de propuestas. Como suele ser el caso de las nuevas funciones en la Web, el orden de implementación y los plazos pueden variar de forma significativa entre los distintos motores. Si quieres usar esas nuevas funciones, debes asegurarte de que ninguno de tus usuarios se quede fuera. En este artículo, obtendrá información acerca de un enfoque para lograrlo.

Algunas funciones nuevas mejoran el tamaño del código incorporando instrucciones nuevas para operaciones comunes, algunas agregan primitivas de rendimiento potentes y otras mejoran la experiencia de los desarrolladores y su integración con el resto de la Web.

Puedes encontrar la lista completa de propuestas y sus respectivas etapas en el repositorio oficial o hacer un seguimiento de su estado de implementación en los motores en la página oficial de hoja de ruta de funciones.

Para garantizar que los usuarios de todos los navegadores puedan usar tu aplicación, tienes que descifrar qué funciones deseas usar. Luego, divídelos en grupos según la compatibilidad con el navegador. Luego, compila tu base de código por separado para cada uno de esos grupos. Por último, en el navegador, debes detectar las funciones compatibles y cargar el paquete correspondiente de JavaScript y Wasm.

Elegir y agrupar atributos

Revisemos esos pasos y elijamos algún conjunto de atributos arbitrario como ejemplo. Supongamos que identifiqué que quiero usar SIMD, subprocesos y control de excepciones en mi biblioteca por razones de tamaño y rendimiento. Su compatibilidad con navegadores es la siguiente:

Una tabla en la que se muestra la compatibilidad del navegador con las funciones elegidas.
Consulta esta tabla de funciones en webassembly.org/roadmap.

Puedes dividir los navegadores en las siguientes cohortes para asegurarte de que cada usuario obtenga la experiencia más optimizada:

  • Navegadores basados en Chrome: se admiten Threads, SIMD y el manejo de excepciones.
  • Firefox: Se admiten Thread y SIMD, pero no el manejo de excepciones.
  • Safari: Se admiten subprocesos, SIMD y no el manejo de excepciones.
  • Otros navegadores: se da por sentado que solo es compatible con el modelo de referencia de WebAssembly.

Este desglose se divide por funciones compatibles en la versión más reciente de cada navegador. Los navegadores modernos son perdurables y se actualizan automáticamente, por lo que, en la mayoría de los casos, solo debes preocuparte por la versión más reciente. Sin embargo, siempre que incluyas WebAssembly de referencia como cohorte de resguardo, puedes proporcionar una aplicación que funcione incluso para usuarios con navegadores desactualizados.

Cómo compilar para diferentes conjuntos de atributos

WebAssembly no tiene una forma integrada de detectar las funciones compatibles en el entorno de ejecución, por lo que todas las instrucciones del módulo deben ser compatibles con el destino. Por este motivo, debes compilar el código fuente en Wasm por separado para cada conjunto de atributos.

Cada cadena de herramientas y sistema de compilación es diferente, y deberás consultar la documentación de tu propio compilador para saber cómo modificar esas funciones. Para simplificar, usaré una biblioteca C++ de un solo archivo en el siguiente ejemplo y mostraré cómo compilarla con Emscripten.

Usaré SIMD a través de la emulación SSE2, subprocesos a través de la compatibilidad con la biblioteca Pthreads y elegiré entre el control de excepciones de Wasm y la implementación de resguardo de JavaScript:

# 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

El código C++ puede usar #ifdef __EMSCRIPTEN_PTHREADS__ y #ifdef __SSE2__ para elegir de forma condicional entre implementaciones paralelas (subprocesos y SIMD) de las mismas funciones y las implementaciones en serie en el tiempo de compilación. Se vería así:

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
}

El control de excepciones no necesita directivas #ifdef, ya que se puede usar de la misma manera desde C++, independientemente de la implementación subyacente elegida a través de las marcas de compilación.

Cómo cargar el paquete correcto

Una vez que hayas creado paquetes para todas las cohortes de funciones, deberás cargar el correcto desde la aplicación principal de JavaScript. Para ello, primero debes detectar qué funciones admite el navegador actual. Puedes hacerlo con la biblioteca wasm-feature-detect. Si lo combinas con la importación dinámica, podrás cargar el paquete más optimizado en cualquier navegador:

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

Palabras finales

En esta publicación, mostré cómo elegir, crear y alternar entre paquetes para diferentes conjuntos de funciones.

A medida que crece la cantidad de atributos,la cantidad de cohortes de atributos puede volverse insostenible. Para solucionar este problema, puedes elegir cohortes de funciones basadas en tus datos de usuarios del mundo real, omitir los navegadores menos populares y dejar que recurran a cohortes un poco menos óptimas. Siempre que tu aplicación siga funcionando para todos los usuarios, este enfoque puede proporcionar un equilibrio razonable entre la mejora progresiva y el rendimiento del entorno de ejecución.

En el futuro, WebAssembly podría tener una forma integrada de detectar las funciones compatibles y alternar entre diferentes implementaciones de la misma función dentro del módulo. Sin embargo, este mecanismo sería en sí mismo una función posterior al MVP que necesitarías detectar y cargar de forma condicional mediante el enfoque anterior. Hasta entonces, este enfoque sigue siendo la única forma de compilar y cargar código con las nuevas funciones de WebAssembly en todos los navegadores.