WebAssembly 功能检测

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

Ingvar Stepanyan
Ingvar Stepanyan

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

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

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

为确保所有浏览器的用户都可以使用您的应用,您需要确定要使用的功能。然后,根据浏览器支持将它们分成多个组。然后,为其中每个组单独编译代码库。最后,在浏览器端,您需要检测支持的功能并加载相应的 JavaScript 和 Wasm 包。

选择功能和分组功能

下面我们以任意特征集为例,介绍这些步骤。假设我已确定出于大小和性能方面的考虑,想在库中使用 SIMD、线程和异常处理。它们的浏览器支持如下:

一个表格,显示浏览器对所选功能的支持情况。
webassembly.org/roadmap 上查看此功能表。

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

  • 基于 Chrome 的浏览器:支持线程、SIMD 和异常处理。
  • Firefox:支持 Thread 和 SIMD,不支持异常处理。
  • Safari:支持 Thread,不支持 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 功能构建和加载代码时,这种方法仍然是唯一方法。