首页
/ Webpack 流式加载 wasm-pack 产物:Rust/WebAssembly ES 模块打包实战(wasm-bindgen-esm 示例深度解析)

Webpack 流式加载 wasm-pack 产物:Rust/WebAssembly ES 模块打包实战(wasm-bindgen-esm 示例深度解析)

2026-09-07 20:08:48作者:廉皓灿Ida

导读

本篇文章以当前仓库中 examples/wasm-bindgen-esm 示例为线索,完整讲解 Webpack 如何通过 experiments.asyncWebAssembly 与模块类型 webassembly/async,把由 Rust(wasm-pack/wasm-bindgen)编译出的 ES Module 封装包当作普通异步模块加载、打包并流式实例化。读完本文,你将掌握:wasm-pack 产物的目录与入口结构、对应的 Webpack 配置写法、打包产物中 async module 包装与 wasm 加载运行时的逐段含义,以及 unoptimized / production 两种模式下的输出差异,可直接迁移到你自己的 Rust + WebAssembly 前端工程中。

示例对应的官方说明位于 template.md:它强调三点核心结论——wasm-pack 会产出 ES 模块封装;该 ES 模块可以像其他异步模块一样通过 import / import() 引入;引入时底层 wasm 文件会以 streaming(流式)方式下载并实例化。本文所有配置与产物分析均以仓库真实文件为准。

一、示例概述:Rust 模块 → ES Module 封装 → 异步模块

examples/wasm-bindgen-esm/
├── example.js              # 业务入口:import { greeting } from "./pkg"
├── webpack.config.js       # 启用 asyncWebAssembly 的配置
├── index.html              # 引入 dist/output.js 的页面
├── pkg/                    # wasm-pack 生成的 npm 包(Rust 编译产物)
│   ├── hi_wasm.js          # ESM 入口:再导出 hi_wasm_bg.js 并从 wasm 导入
│   ├── hi_wasm_bg.js       # 胶水层:字符串编解码 + wasm 导出函数封装
│   ├── hi_wasm_bg.wasm     # Rust 编译出的 wasm 二进制
│   ├── hi_wasm.d.ts        # TypeScript 类型
│   ├── hi_wasm_bg.wasm.d.ts
│   └── package.json        # type: module,sideEffects: false
├── README.md               # 该示例的完整成文(含打包输出内联)
└── test.filter.js          # 测试过滤器:无 WebAssembly 环境则跳过

这个示例要解决的场景是:把 Rust(wasm-bindgen)编写的函数编译成能在前端使用的 wasm 产物,并让 Webpack 以 异步模块语义 正确打包它。wasm 二进制体积、实例化时机都不同于普通 JS,因此 Webpack 需要一套专门的模块类型与运行时支持——这正是 examples/wasm-bindgen-esm/template.md 想演示的能力。

wasm-pack 产物内部:ESM 封装的三层结构

先看 pkg/hi_wasm.js,它是 wasm-pack 产物在浏览器端的真正入口,只有两行:

import * as wasm from "./hi_wasm_bg.wasm";
export * from "./hi_wasm_bg.js";

也就是说,wasm-bindgen 生成的 ES 包对外暴露 JS API(greeting 函数),但其实现依赖对 .wasm 文件的 ESM 导入。业务代码只需按常规 ESM 语法 import 即可,例如 example.js

import { greeting } from "./pkg";

document.write(greeting('Bob'));

./pkg 会解析到 pkg/package.jsonmain 指向 hi_wasm.js)。胶水层 pkg/hi_wasm_bg.js 内部实现了 JS↔Wasm 的字符串传递:用 TextEncoder/TextDecoder 配合 __wbindgen_malloc/__wbindgen_realloc/__wbindgen_free 做内存拷贝,greeting(name) 先入栈指针再调用 wasm 导出,最后从共享内存读回结果字符串。类型声明见 pkg/hi_wasm.d.tsgreeting(name: string): string。示例页面 index.html 最终加载打包产物 dist/output.js,把问候语写入页面。

二、Webpack 配置全解:asyncWebAssembly 的开启方式

examples/wasm-bindgen-esm/webpack.config.js 是整个示例的"开关"所在,完整代码如下:

"use strict";

/** @type {import("webpack").Configuration} */
const config = {
	// mode: "development || "production",
	output: {
		webassemblyModuleFilename: "[hash].wasm",
		publicPath: "dist/"
	},
	module: {
		rules: [
			{
				test: /\.wasm$/,
				type: "webassembly/async"
			}
		]
	},
	optimization: {
		chunkIds: "deterministic" // To keep filename consistent between different modes (for example building only)
	},
	experiments: {
		asyncWebAssembly: true
	}
};

module.exports = config;

1. experiments.asyncWebAssembly: true 是总开关

从仓库源码看,该实验特性在默认配置与规范化逻辑中贯穿多层:在 lib/config/defaults.jsasyncWebAssembly 作为 experiments 属性存在,并且模块规则默认值列表中明确包含 "asyncWebAssembly"(见 lib/config/defaults.js)。只有开启它,.wasm 文件才会被作为异步 wasm 模块处理,并注入对应的加载运行时。在默认情况下若你看到 EnvironmentNotSupportAsyncWarning 相关提示,可参考 lib/errors/EnvironmentNotSupportAsyncWarning.js 追溯原因。

2. module.rulestype: "webassembly/async"

规则把所有 .wasm 文件标记为异步 wasm 模块类型(对应常量见 lib/ModuleTypeConstants.js 中的 WEBASSEMBLY_MODULE_TYPE_ASYNC)。在 lib/WebpackOptionsApply.js 中,当 experiments.asyncWebAssembly 开启时会应用 lib/wasm-async/AsyncWebAssemblyModulesPlugin.js,该插件为 async wasm 提供完整的模块封装与运行时支持(模块 AsyncWasmModule、解析器 AsyncWasmParser 等都在 lib/wasm-async 目录下)。在 Webpack 5 的现代用法中,也可以直接用内置的自动规则而省略手写 test,只要 experiments.asyncWebAssembly 开启即可。

3. output.webassemblyModuleFilenamepublicPath

  • webassemblyModuleFilename: "[hash].wasm":定义 wasm 文件的输出文件名模板,[hash] 会替换为内容哈希,构建出的实际文件名形如 83f766418938094a1584.wasm
  • publicPath: "dist/":与 index.html<script src="dist/output.js"> 对应,保证运行时 fetch wasm 的 URL 拼装正确(详见下文"wasm 加载运行时"中 fetch("dist/" + ... + ".wasm") 的实际生成代码)。

4. optimization.chunkIds: "deterministic"

配置注释说明了动机:保证两种模式下产物文件名一致(例如"仅构建"测试场景),便于对 unoptimized 与 production 输出做稳定对比,也与 README 中两种模式统计输出一致的要求吻合。

三、打包产物逐段剖析:dist/output.js 如何"封装"异步模块

template.md 的第 12–16 行展示了构建产物的结构(占位符 _{{dist/output.js}}_ 在构建时被真实产物替换,仓库 README.md 中可见完整内联产物)。下面按模块编号逐段解读生成后的 examples/wasm-bindgen-esm 输出(非生产模式)。

模块 0:入口的 async 包装

./example.js 被编译成一个"异步模块",模块函数体内出现关键调用 __webpack_require__.a(module, async (...) => { ... })。这正是 async module 运行时的入口标记:模块主体异步执行,遇到异步依赖时通过 __webpack_handle_async_dependencies__ 等待解析,结束时调用 __webpack_async_result__() 通知完成。可见 import "./pkg"(pkg 内部依赖 wasm,是异步依赖)被自动识别,入口因此升级为 async 语义。

模块 1:wasm-bindgen 胶水层 ./pkg/hi_wasm_bg.js

产物同样以 __webpack_require__.a(...) 包装,并用 __webpack_require__.d(define property getters)声明 harmony 导出 greeting。真正关键的是其内部一行:

var _hi_wasm_bg_wasm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hi_wasm_bg.wasm */ 2);

即胶水层对 .wasm 文件的 ESM 导入被 Webpack 映射为对 模块 2(wasm 模块)的运行时依赖,并同样走异步依赖等待逻辑。之后才执行 getUint8Memory0/passStringToWasm0/greeting 等原胶水实现。这也印证了 pkg/hi_wasm.jsimport * as wasm from "./hi_wasm_bg.wasm" 会被 Webpack 正确处理。

模块 2:wasm 模块的加载占位

wasm 模块被压缩为一行导出占位:

module.exports = __webpack_require__.v(exports, module.id, "83f766418938094a1584");

__webpack_require__.v 是运行时提供的 wasm 加载器,第三个参数即 wasm 输出文件名(内容哈希)。产物注释列表显示该 wasm 导出 greetingmemory__wbindgen_malloc 等,其中"provision prevents renaming"提示:这些 wasm 导出名是 wasm 二进制固定的,不能被压缩器重命名。

运行时三段代码的作用

产物尾部的 runtime 代码(README 中以 <details> 折叠展示,template.md 对应 dist/output.js 的运行时部分)由三块核心组成:

  1. 模块缓存与 __webpack_require__:标准模块加载器骨架;
  2. __webpack_require__.a(async module 运行时):以 webpackQueues/webpackExports 两个 Symbol 标记 Promise,实现"一个模块的 exports 可能迟到(pending)"的异步传播语义。wrapDeps 会把普通模块与 Promise 依赖统一包装成队列节点,所有依赖就绪后回调 resolveQueue。入口与胶水层函数体内的 __webpack_handle_async_dependencies__ 正来自这里;
  3. __webpack_require__.v(wasm loading 运行时):见下节。

启动代码最后以 __webpack_require__(0) 触发整个异步链。

四、wasm 加载运行时:fetch + instantiateStreaming 的两级降级策略

template.md 第 4 行点明核心特性:"底层 wasm 模块以 streaming 方式下载并实例化"。这一行为落在产物 runtime 的 __webpack_require__.v 上:

var req = fetch("dist/" + "" + wasmModuleHash + ".wasm");
var fallback = () => (req
    .then((x) => (x.arrayBuffer()))
    .then((bytes) => (WebAssembly.instantiate(bytes, importsObj)))
    .then((res) => (Object.assign(exports, res.instance.exports))));
return req.then((res) => {
    if (typeof WebAssembly.instantiateStreaming === "function") {
        return WebAssembly.instantiateStreaming(res, importsObj)
            .then(
                (res) => (Object.assign(exports, res.instance.exports)),
                (e) => {
                    if(res.headers.get("Content-Type") !== "application/wasm") {
                        console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
                        return fallback();
                    }
                    throw e;
                }
            );
    }
    return fallback();
});

解读如下:

  • URL 拼装fetch("dist/" + hash + ".wasm") 直接验证了 output.webassemblyModuleFilename: "[hash].wasm"publicPath: "dist/" 的配合;
  • 优先流式:环境支持 WebAssembly.instantiateStreaming 时,fetch 响应流边下载边编译,显著缩短首屏可用时间;
  • 一级降级:若流式实例化失败,且服务器未把 wasm 响应标为 application/wasm MIME 类型,则回退到 fetch → arrayBuffer → WebAssembly.instantiate 的慢路径,并打印清晰的 console.warn
  • 二级降级:浏览器不支持 instantiateStreaming 时直接走 fallback。

这与源码层实现一一对应:不同目标运行时分别提供 __webpack_require__.v 的生成策略,如浏览器端的 lib/web/FetchCompileAsyncWasmPlugin.js、Node 端的 lib/node/ReadFileCompileAsyncWasmPlugin.js 与通用路径 lib/wasm-async/UniversalCompileAsyncWasmPlugin.js,统一生成逻辑在 lib/wasm-async/AsyncWasmLoadingRuntimeModule.js(支持流式的关键参数即其 supportsStreaming 选项,见 AsyncWasmLoadingRuntimeModule.js)。wasm 文件名模板的哈希解析实现位于 lib/wasm/wasmModuleFilename.js

五、构建输出对比:Unoptimized 与 Production mode

template.md 的 Info 部分给出两种模式的 stats(仓库 README.md 中为真实构建结果,模板中以 _{{stdout}}__{{production:stdout}}_ 占位)。核心差异如下。

Unoptimized(development)

asset 83f766418938094a1584.wasm 14.8 KiB [emitted] [immutable] (auxiliary name: main)
asset output.js 13.3 KiB [emitted] (name: main)
chunk (runtime: main) output.js (main) 3.03 KiB (javascript) 14.8 KiB (webassembly) 3.49 KiB (runtime) [entry] [rendered]
  > ./example.js main
  runtime modules 3.49 KiB 5 modules
  dependent modules 2.97 KiB (javascript) 14.8 KiB (webassembly) [dependent] 2 modules
  ./example.js 69 bytes [built] [code generated]
    [no exports]
    [used exports unknown]
    entry ./example.js main
webpack X.X.X compiled successfully

Production mode

asset d9d80d430272dc67db6b.wasm 14.8 KiB [emitted] [immutable] (auxiliary name: main)
asset output.js 3.25 KiB [emitted] [minimized] (name: main)
chunk (runtime: main) output.js (main) 3.03 KiB (javascript) 14.8 KiB (webassembly) 3.28 KiB (runtime) [entry] [rendered]
  > ./example.js main
  runtime modules 3.28 KiB 4 modules
  dependent modules 2.97 KiB (javascript) 14.8 KiB (webassembly) [dependent] 2 modules
  ./example.js 69 bytes [built] [code generated]
    [no exports]
    [no exports used]
    entry ./example.js main
webpack X.X.X compiled successfully

要点:

  • wasm 文件独立成资产,标注 [immutable](内容哈希文件名保证长期缓存友好),大小固定 14.8 KiB,与 JS 分离统计为 14.8 KiB (webassembly)
  • 两种模式 wasm 哈希不同83f7… vs d9d8…),这是 webpack 版本/模式对编译哈希的影响,也解释了为何示例要固定 chunkIds: "deterministic" 以保证对比一致;
  • production 的 output.js 被 minify(13.3 KiB → 3.25 KiB),runtime 模块从 5 个缩到 4 个、体积 3.49 → 3.28 KiB;
  • 两处注释 [no exports](入口模块不导出)与 [no exports used](生产模式下确无导出被使用)是 webpack 对导出使用分析的标注;
  • wasm 文件的 [auxiliary name: main] 表示它作为主 chunk 的辅助资产存在,[dependent] 标记说明 2 个依赖模块都挂在入口之下。

六、运行与验证方式

本示例从 npm 包结构(main/types)到 Webpack 解析完全走 ESM 语义,可直接在支持 WebAssembly 的现代浏览器中运行:

  1. 以仓库 examples/wasm-bindgen-esm 为工作目录,按该目录下 webpack.config.js 构建,或参考 examples/README.md 中 examples 的统一构建脚本;
  2. 构建后得到 dist/output.js 与对应的 *.wasm(同时需把 wasm 输出复制到 dist/ 下,保持 webpack.config.jspublicPath: "dist/" 指向的目录结构);
  3. 通过本地 HTTP 服务打开 index.html(务必用 HTTP 而非 file://,否则 fetch wasm 会因 CORS 失败),观察页面输出问候语;
  4. 打开开发者工具确认:wasm 请求由运行时 fetch 发出,网络面板能看到 instantiateStreaming 生效;若静态服务器未配置 application/wasm MIME,控制台会打印 README/产物中那段降级警告。

测试前提

仓库配套的 test.filter.js 展示了该示例纳入测试套件的前提:

"use strict";

const supportsWebAssembly = require("../../test/helpers/supportsWebAssembly");

module.exports = () => supportsWebAssembly();

即在当前执行环境不支持 WebAssembly 时跳过该用例(test/helpers/supportsWebAssembly 可在 test/helpers 中找到)。这提示了一个工程约束:async WebAssembly 与 streaming 编译依赖运行环境能力,在你自己的项目中引入前应做能力检测或提供降级方案。

七、总结:一条可复用的 Rust→Webpack 接入路径

回到 examples/wasm-bindgen-esm/template.md 的结论,本示例实际上串起了这样一条完整链路:

  1. Rust 侧:用 wasm-pack(wasm-bindgen)编译,产出带 ESM 入口(hi_wasm.js)、胶水层(hi_wasm_bg.js)与类型声明(*.d.ts)的标准 npm 包,wasm 二进制独立存放;
  2. 业务侧:与导入任何 ES 异步模块一致,import { greeting } from "./pkg" 即可,无需感知底层 wasm 细节;
  3. 构建侧:开启 experiments.asyncWebAssembly + .wasm 规则 type: "webassembly/async",并配合 output.webassemblyModuleFilename / publicPath 指定 wasm 输出命名与可访问路径;
  4. 运行时:Webpack 生成的 __webpack_require__.v 完成 fetch 与流式实例化,包含对 MIME 类型错误与不支持环境的自动降级。

当你需要把 Rust 编写的计算密集逻辑(如加密、编解码、图像处理)接入 Webpack 前端工程时,将本示例的 pkg 结构、入口导入写法与 webpack.config.js 三件套照搬到自己的工程,即可获得同样"像 import 一个普通模块一样使用 wasm"的开发体验,同时保留流式实例化带来的加载性能收益。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388