WebAssembly 功能检测

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

Ingvar Stepanyan
Ingvar Stepanyan

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

有些新功能通过为常见操作添加新指令来缩减代码大小,有些新功能会添加强大的性能基元,还有一些新功能会改进开发者体验并提升与 Web 的其他部分的集成效果。

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

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

选择功能和分组功能

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

显示所选功能的浏览器支持的表格。
如需查看此功能表,请访问 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++ 以相同的方式使用它。

加载正确的 bundle

为所有特征同类群组构建软件包后,您需要从主要 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 功能构建和加载代码的唯一方法仍然是这种方法。