Aprende a usar las funciones más recientes de WebAssembly y, al mismo tiempo, brindar compatibilidad con los 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 a través del proceso de estandarización de propuestas. Como suele ocurrir con las funciones nuevas en la Web, el orden de implementación y los cronogramas pueden diferir significativamente entre los diferentes motores. Si quieres usar esas funciones nuevas, debes asegurarte de que ninguno de tus usuarios se quede sin ellas. En este artículo, aprenderás un enfoque para lograrlo.
Algunas funciones nuevas mejoran el tamaño del código agregando instrucciones nuevas para operaciones comunes, otras agregan primitivas de rendimiento potentes 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 de su estado de implementación en los motores en la página oficial del plan de ruta de las funciones.
Para asegurarte de que los usuarios de todos los navegadores puedan usar tu aplicación, debes determinar 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 de JavaScript y Wasm correspondiente.
Cómo seleccionar y agrupar componentes
Para ello, tomemos un conjunto de atributos arbitrario como ejemplo. Digamos 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:
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 el manejo de excepciones.
- Firefox: Se admiten subprocesos y SIMD, pero no el manejo de excepciones.
- Safari: Se admiten subprocesos, SIMD y no el manejo de excepciones.
- Otros navegadores: Se supone que solo se admite WebAssembly de referencia.
Este desglose se divide según la compatibilidad con las 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 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.
Compila para diferentes conjuntos de funciones
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 esos diferentes conjuntos de funciones.
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 ajustar esas funciones. Para simplificar, usaré una biblioteca de 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 de SSE2, subprocesos a través de la compatibilidad con la biblioteca de Pthreads y elegiré entre el manejo de excepciones de Wasm y la implementación de JavaScript de resguardo:
# 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 que se elija a través de las marcas de compilación.
Cargando el paquete correcto
Una vez que hayas creado paquetes para todas las cohortes de funciones, debes cargar el correcto desde la aplicación principal de JavaScript. Para ello, primero detecta 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
Palabras finales
En esta publicación, te mostré cómo elegir, compilar y cambiar entre paquetes para diferentes conjuntos de funciones.
A medida que aumenta la cantidad de atributos, es posible que la cantidad de cohortes de atributos no se pueda mantener. Para solucionar este problema, puedes elegir cohortes de funciones en función de tus datos de usuarios del mundo real, omitir los navegadores menos populares y permitir que se recurran a cohortes ligeramente 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, es posible que WebAssembly tenga una forma integrada de detectar funciones compatibles y cambiar entre diferentes implementaciones de la misma función dentro del módulo. Sin embargo, ese mecanismo sería una función posterior al MVP que deberías detectar y cargar de forma condicional con 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.