你为了跑一个 wasm,往 CSP 里加了三年的 unsafe-eval——今天 TC39 把编译这件事从模块系统里彻底还回来了
你为了跑一个 wasm,往 CSP 里加了三年的 unsafe-eval——今天 TC39 把编译这件事从模块系统里彻底还回来了
线上 CSP 加 wasm-unsafe-eval 这件事,我知道很多团队加完就没再回头看。今天这篇文章想说的是:从 TC39 到 Chrome、Firefox、Deno、Rspack,整条链路已经把「编译 wasm」这件事搬回了 ES 模块系统——你那条指令,很可能已经可以撤了。
先说清楚为什么这口黑锅扣了这么多年。
CSP 的 script-src 里,编译 WebAssembly 被归到「代码求值」一类。没放行的时候,控制台报的是这句:
CompileError: WebAssembly.instantiateStreaming(): Compiling or instantiating
WebAssembly module violates the following Content Security Policy directive
because 'unsafe-eval' is not an allowed source of script...
于是大家的做法一模一样:往 CSP 里补一条。
script-src 'self' 'wasm-unsafe-eval'
这个补丁在 MV3 扩展里还有个更硬的约束:script-src / object-src / worker-src 只接受 self、none、wasm-unsafe-eval,写 unsafe-eval 会让扩展直接装不上。所以严格来说,wasm-unsafe-eval 已经是当前最干净的近似解——它不是 unsafe-eval,只放开 wasm 编译,不放开 eval()。
但「最干净的近似解」本身就是在提醒你:安全团队盯的是这一条指令,你每加一次都要开口子,每撤一次都要验证一遍。
Source Phase Imports 换的是另一条路:让 wasm 的编译产物走模块系统,从源头上不构成「用户侧求值」。
这条新语法长什么样
静态形式就是在 import 后面加一个 phase:
import source myModuleSource from "./my-module.wasm";
拿到的 myModuleSource 是一个已经编译好的 WebAssembly.Module:
import source myModuleSource from "./my-module.wasm";
const instance = await WebAssembly.instantiate(myModuleSource, {
env: { log: console.log },
});
const { exports } = instance;
动态形式是 import.source(),注意这是「元属性」形态的特殊语法,不是 import 对象上的方法:
const myModuleSource = await import.source("./my-module.wasm");
const instance = await WebAssembly.instantiate(myModuleSource);
原文里有两句话值得逐字读:模块被 fetch 并编译,但它的依赖不会被加载,也不会被 link 或 evaluate;模块加载并编译成功后,promise 兑现为一个 AbstractModuleSource 对象。
这就是整个提案的关键——它把模块流水线切了一刀,只跑前半段。官方把这条流水线拆成五步:解析网络路由、fetch 并编译、source 阶段(拿到执行上下文、知道依赖该从哪加载)、link、evaluate。import source 停在第 2.5 步。
为什么它不是「又一个语法糖」
如果你只把 fetch + compile 换成一行 import,那确实只是省了几行。真正的价值在三个地方。
第一,CSP 里的那条指令可以退休。 提案的 Security Benefits 段落写得很直接:原生 ES 模块加载器能够实施安全策略(包括浏览器里的 CSP),把模块系统已有的静态安全收益延伸到自定义加载器上,本身就是安全收益;对 Wasm 而言,这意味着动态实例化可以有 source 级别的 CSP 策略。提案的 Q&A 里更干脆:
Why not just use
const module = await WebAssembly.compileStreaming(fetch(new URL("./module.wasm", import.meta.url)));
…… Secondly when using CSP,script-src: unsafe-evalwould not be needed.
第二,一份编译产物,多个实例。 这是我在项目里最想要的能力。以前想在同一份 wasm 上跑两组不同的 imports(比如两个 worker 各持一份独立线性内存),你要么重新起一份二进制,要么自己维护一个 WebAssembly.Module 的缓存 Map。现在模块加载器替你缓存:
import source imageProcessor from "./image-processor.wasm";
// 两个 worker,各拿一份实例、各自的内存
const worker1 = await WebAssembly.instantiate(imageProcessor, {
env: { memory: new WebAssembly.Memory({ initial: 256 }) },
});
const worker2 = await WebAssembly.instantiate(imageProcessor, {
env: { memory: new WebAssembly.Memory({ initial: 256 }) },
});
缓存语义在提案里也有明确规定:[[ModuleSourceObject]] 以基础模块记录为键,所以它总是唯一对应被导入的那个模块。同一个 specifier 再 import 一次 source,拿到的是同一个对象。
第三,静态可分析。 手动 fetch() 是运行时的东西,打包器看不到你在加载谁。import source 是静态声明,进得了模块图——树摇、依赖分析、构建期预编译都能覆盖到 .wasm,Wasm 才算真正变成「一种模块格式」。
谁已经能用了
- TC39 状态: source phase imports 提案 Stage 3,Champions 是 Luca Casonato 和 Guy Bedford,Stage 3 reviewers 是 Daniel Ehrenberg 和 Kris Kowal,上游 PR 是 tc39/ecma262#3492。提案只规定
source这一个 phase,import.asset之类的留给未来。 - Firefox: Mozilla 的 intent to prototype and ship 公告(2026 年 6 月)写明「As of Firefox 153, I intend to turn Source Phase Imports on by default」,此前一直藏在
javascript.options.experimental.source_phase_imports后面。 - Deno / Node.js: WASM 的 source phase 已经实现,Deno 2.x 开箱可用。
- 打包器: Rspack 的
experiments.sourceImport(默认 false,显式开启),Rslib 的 .wasm 原生支持里直接把它列为「Source phase 导入」用法;Vite 这条线目前仍需要 wasm 插件(vite-plugin-wasm 提供 ESM integration,source phase 支持还在推进)。 - 浏览器覆盖面: Chrome 131+ / Edge 131+ 已 ship 并默认开启;Firefox 153 起默认开启;Safari / WebKit 侧还没有实现,对应的 bug 是 webkit.org #274908,浏览器兼容数据目前仍标「Not supported」。所以 Web 端要留 fallback。
一个容易翻车的点:这是两个提案
import source 这个关键字被两个提案共用,很多人会搞混:
- WASM source phase(也就是本文讲的,tc39/proposal-source-phase-imports):Stage 3,有实际 ship 的实现,返回
WebAssembly.Module。 - ESM phase imports for JavaScript(tc39/proposal-esm-phase-imports):Stage 2.7,还没有 ship 的实现,返回的是未 link 的
AbstractModuleSource,目标是 worker 启动和虚拟化模块加载器。
所以「我查到有人支持 import source 了」这句话,对 JS 模块并不成立。目前只有 WASM 模块支持 source phase,而得到的就是 WebAssembly.Module 对象。另外 TypeScript 目前还解析不了 import source 和 import.source(),要出类型声明文件就只能在 JS 文件里写这两个语法。
还有一个使用限制来自规范文本:只支持 default import 形式。import source { property } from "./my-module.wasm" 这种写法不行。另外 source 不是保留字,import source from "./module.js" 仍然是「默认导入一个名叫 source 的绑定」,不会走新语义。
你现在可以做的三步
- 先别急着删 CSP。 Safari / WebKit 还没实现,线上仍然需要 fallback 路径。建议保留
instantiateStreaming()那条分支,把import source作为现代路径走前面。 - 在你能自主选运行时的环境先上。 Node.js 服务端、Deno、Electron 这类运行时是你自己定的,source phase 已经可用,用来验证多实例和 worker 分发最省事——顺便把那个手写的
WebAssembly.Module缓存 Map 删掉。 - 给打包器做一层能力探测。 Rspack 里先开
experiments.sourceImport: true跑一遍构建,看产物里的 wasm 是不是变成了编译产物引用;Vite 项目先别动,等 source phase 插件稳定。
wasm 被当成 eval 来处理这件事,本身就是历史包袱——模块系统本来就有更好的位置安放它。现在位置有了,剩下的是把线上那条 wasm-unsafe-eval 摘干净,以及确认你的用户浏览器里有一个能接住的 fallback。
证据来源
- TC39 提案原文:github.com/tc39/proposal-source-phase-imports(Stage 3,Security Benefits / Cache Key Semantics / Q&A 段落)
- MDN 与 DevDocs 语法文档:
import source声明形式与import.source()运算符,含「仅 WASM 支持、返回 AbstractModuleSource / WebAssembly.Module」与 TypeScript 限制说明 - Mozilla dev-platform intent to ship(2026-06):Firefox 153 默认开启,
javascript.options.experimental.source_phase_imports - esmodules.com「WebAssembly & ESM」:Chrome 131+ / Edge 131+ 已 ship,Node.js 与 Deno 2.x 已实现,Firefox / Safari 无实现,两个同关键字提案的区别
- Rspack / Rslib 官方文档:
experiments.sourceImport、.wasm 的 source phase 用法 - CSP 报错现场样本:SAP Help Portal、Anki 论坛、Thinkwise 社区、extshield(MV3 扩展
wasm-unsafe-eval三条硬约束)
评论区
登录后可评论。