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í. Se agregan funciones nuevas mediante el proceso de estandarización de propuestas. Como suele ser el caso de las funciones nuevas en la Web, el orden y los cronogramas de implementación pueden diferir significativamente entre los diferentes motores. Si quieres usar esas funciones nuevas, debes asegurarte de que ninguno de tus usuarios se quede fuera. En este artículo, obtendrás información sobre el enfoque que puedes adoptar para lograrlo.

Algunas funciones nuevas mejoran el tamaño del código agregando instrucciones nuevas para operaciones comunes, algunas agregan potentes primitivas de rendimiento y otras mejoran la experiencia del desarrollador y la 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 del estado de su implementación en los motores en la página oficial de hoja de ruta de funciones.

Para asegurarte de que los usuarios de todos los navegadores puedan usar tu aplicación, debes descubrir qué funciones quieres 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 lado del navegador debes detectar las funciones compatibles y cargar el paquete de JavaScript y Wasm correspondiente.

Cómo elegir y agrupar funciones

Analicemos esos pasos eligiendo un conjunto de atributos arbitrario como ejemplo. Supongamos que identifiqué que quiero usar SIMD, subprocesos y manejo 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 de los navegadores con las funciones elegidas.
Consulta esta tabla de funciones en webassist.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 subprocesos, SIMD y manejo de excepciones.
  • Firefox: Se admiten Thread y SIMD, pero no el manejo de excepciones.
  • Safari: Se admiten subprocesos, SIMD y el control de excepciones, no.
  • Otros navegadores: Se supone que solo es compatible con el modelo de referencia de WebAssembly.

Este desglose se divide por compatibilidad de funciones 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 tienes que preocuparte por la actualización más reciente. Sin embargo, siempre que incluyas WebAssembly de referencia como una cohorte de resguardo, aún 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 funciones compatibles en el entorno de ejecución, por lo que todas las instrucciones del módulo deben ser compatibles con el destino. Por lo tanto, debes compilar el código fuente en Wasm por separado para cada uno de los diferentes conjuntos 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 manejo de excepciones 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++ en sí puede usar #ifdef __EMSCRIPTEN_PTHREADS__ y #ifdef __SSE2__ para elegir condicionalmente entre implementaciones paralelas (subprocesos y SIMD) de las mismas funciones y las implementaciones seriales 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.

Cargando el paquete correcto

Una vez que hayas creado los paquetes para todas las cohortes de funciones, debes cargar el correcto desde la aplicación principal de JavaScript. Para hacer eso, primero debes detectar qué funciones son compatibles con el navegador actual. Puedes hacerlo con la biblioteca wasm-feature-detect. Si lo combinas con la importación dinámica, puedes 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

Últimas palabras

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

A medida que crece la cantidad de atributos,es posible que la cantidad de cohortes de atributos se vuelva insostenible. Para solucionar este problema, puedes elegir cohortes de funciones basadas en tus datos de usuario del mundo real, omitir los navegadores menos populares y dejar que vuelvan 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 tiempo de ejecución.

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