WebAssembly 功能检测

了解如何使用最新的 WebAssembly 功能,同时为所有浏览器的用户提供支持。

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly 1.0 是在四年前发布的,但开发并没有就此止步。新功能通过提案标准化流程添加。与网络上的新功能一样,不同引擎中新功能的实现顺序和时间表可能会有很大差异。如果您想使用这些新功能,需要确保不会遗漏任何用户。在本文中,您将了解实现这一目标的方法。

一些新功能通过为常见操作添加新的指令来缩减代码大小,一些添加强大的性能基元,还有一些功能用于改善开发者体验以及与网络其余部分的集成。

您可以在官方代码库中找到提案及其各自阶段的完整列表,也可以在官方功能路线图页面上跟踪这些提案在引擎中的实现状态。

为确保所有浏览器的用户都能使用您的应用,您需要弄清楚要使用哪些功能。然后,根据浏览器支持将它们分组。然后,为每个组单独编译代码库。最后,在浏览器端,您需要检测支持的功能并加载相应的 JavaScript 和 Wasm 软件包。

选择功能和分组功能

下面我们选择任意特征集作为示例,了解一下这些步骤。假设我已经确定出于大小和性能方面的原因,想要在我的库中使用 SIMD、线程和异常处理。其浏览器支持如下:

<ph type="x-smartling-placeholder">
</ph> 显示所选功能的浏览器支持的表格。 <ph type="x-smartling-placeholder">
</ph> 您可以在 webassembly.org/roadmap 上查看此功能表。

您可以将浏览器划分为以下同类群组,以确保每位用户都能获得最优化的体验:

  • 基于 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 功能构建和加载代码的唯一方式。