了解如何在支持所有浏览器的用户的同时使用最新的 WebAssembly 功能。
WebAssembly 1.0 于四年前发布,但开发并未就此停止。新功能通过 提案标准化流程 添加。与 web 上的新功能通常情况一样,它们在不同引擎中的实现顺序和时间表可能差异很大。如果您想使用这些新功能,则需要确保不会遗漏任何用户。在本文中,您将学习一种实现此目标的方法。
一些新功能通过为常用操作添加新指令来改进代码大小,一些添加强大的性能原语,另一些则改进开发人员体验以及与 web 其余部分的集成。
您可以在 官方仓库 中找到提案的完整列表及其各自的阶段,或在官方 功能路线图 页面上跟踪它们在引擎中的实现状态。
为了确保所有浏览器的用户都可以使用您的应用程序,您需要确定要使用的功能。然后,根据浏览器支持将它们分成几组。然后,为每组分别编译代码库。最后,在浏览器端,您需要检测支持的功能并加载相应的 JavaScript 和 Wasm 包。
选择和分组功能
让我们通过选择一些任意功能集作为示例来了解这些步骤。假设我已经确定要在我的库中使用 SIMD、线程和异常处理,以提高大小和性能。它们的 浏览器支持 如下:

您可以将浏览器分为以下几个组群,以确保每个用户都能获得最佳体验:
- 基于 Chrome 的浏览器:完全支持线程、SIMD 和异常处理。
- Firefox:支持线程和 SIMD,但不支持异常处理。
- Safari:支持线程,但不支持 SIMD 和异常处理。
- 其他浏览器:假设仅支持基线 WebAssembly。
此分类按每个浏览器的最新版本中的功能支持进行划分。现代浏览器是常青的并且会自动更新,因此在大多数情况下,您只需要担心最新版本。但是,只要您包含基线 WebAssembly 作为回退组群,即使对于使用过时浏览器的用户,您仍然可以提供可用的应用程序。
为不同的功能集编译
WebAssembly 没有内置的方法来检测运行时支持的功能,因此模块中的所有指令都必须在目标上受支持。因此,您需要为每个不同的功能集将源代码单独编译为 Wasm。
每个工具链和构建系统都不同,您需要查阅编译器文档,了解如何调整这些功能。为了简单起见,在以下示例中,我将使用一个单文件 C++ 库,并演示如何使用 Emscripten 编译它。
我将通过 SSE2 模拟 使用 SIMD,通过 Pthreads 库支持使用线程,并在 Wasm 异常处理 和 回退 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
C++ 代码本身可以使用 #ifdef __EMSCRIPTEN_PTHREADS__
和 #ifdef __SSE2__
在编译时有条件地选择相同函数的并行(线程和 SIMD)实现和串行实现。它看起来像这样:
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
}
异常处理不需要 #ifdef
指令,因为它可以通过 C++ 以相同的方式使用,而无需考虑通过编译标志选择的底层实现。
加载正确的包
为所有功能组群构建包后,您需要从主 JavaScript 应用程序加载正确的包。为此,首先,检测当前浏览器中支持哪些功能。您可以使用 wasm-feature-detect 库来做到这一点。通过将其与 动态导入 结合使用,您可以在任何浏览器中加载最优化的包。
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
总结
在这篇文章中,我展示了如何为不同的功能集选择、构建和切换包。
随着功能数量的增长,功能组群的数量可能会变得难以维护。为了缓解这个问题,您可以根据您的真实用户数据选择功能组群,跳过不太流行的浏览器,并让它们回退到稍微不太理想的组群。只要您的应用程序仍然适用于所有用户,这种方法就可以在渐进增强和运行时性能之间提供合理的平衡。
将来,WebAssembly 可能会获得内置的方法来检测支持的功能,并在模块内切换同一函数的不同实现。但是,这种机制本身将是一项 MVP 后的功能,您需要使用上述方法有条件地检测和加载它。在此之前,此方法仍然是在所有浏览器中构建和加载使用新的 WebAssembly 功能的代码的唯一方法。