了解如何使用最新的 WebAssembly 功能,同时为所有浏览器的用户提供支持。
WebAssembly 1.0 是在四年前发布的,但开发并没有就此止步。新功能通过提案标准化流程添加。与网络上的新功能一样,不同引擎中新功能的实现顺序和时间表可能会有很大差异。如果您想使用这些新功能,需要确保不会遗漏任何用户。在本文中,您将了解实现这一目标的方法。
一些新功能通过为常见操作添加新的指令来缩减代码大小,一些添加强大的性能基元,还有一些功能用于改善开发者体验以及与网络其余部分的集成。
您可以在官方代码库中找到提案及其各自阶段的完整列表,也可以在官方功能路线图页面上跟踪这些提案在引擎中的实现状态。
为确保所有浏览器的用户都能使用您的应用,您需要弄清楚要使用哪些功能。然后,根据浏览器支持将它们分组。然后,为每个组单独编译代码库。最后,在浏览器端,您需要检测支持的功能并加载相应的 JavaScript 和 Wasm 软件包。
选择功能和分组功能
下面我们选择任意特征集作为示例,了解一下这些步骤。假设我已经确定出于大小和性能方面的原因,想要在我的库中使用 SIMD、线程和异常处理。其浏览器支持如下:
<ph type="x-smartling-placeholder">![显示所选功能的浏览器支持的表格。](https://web.developers.google.cn/static/articles/webassembly-feature-detection/image/a-table-showing-browser-s-d37cca41e05a3.png?authuser=7&hl=ro)
您可以将浏览器划分为以下同类群组,以确保每位用户都能获得最优化的体验:
- 基于 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 库。通过将其与动态导入结合使用,您可以在任何浏览器中加载最优化的 bundle:
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 功能构建和加载代码的唯一方式。