Mozilla 近日做出一个标志性决定:Firefox 148 将默认禁用 SpiderMonkey 引擎中的 asm.js 优化,后续版本计划彻底移除相关代码。asm.js——这项曾让 JavaScript 跑出接近原生速度的"黑客级"技术实验,终于走到了终点。它的退出不是失败,而是 WebAssembly 已经站稳脚跟的信号:交接完成,老兵可以离场了。
asm.js 做了什么,为什么它必须退场
2013 年,asm.js 的提出本质上是一个妥协方案:在浏览器没有原生二进制执行能力的前提下,用 JavaScript 的一个严格子集来模拟低级语言的行为。开发者把 C/C++ 编译成一种特殊格式的 JavaScript——所有变量只使用整数或浮点数类型,所有运算都标注类型,整个模块以 "use asm" 声明开头。引擎识别到这些约束后,可以跳过类型推断和 JIT 的很多中间步骤,直接生成接近机器码的优化路径。
这套方案确实有效。Emscripten 早期把游戏引擎、物理模拟等重型计算编译成 asm.js,在 Firefox 上跑出了令人惊讶的性能。但代价也很明显:
- asm.js 本质上还是 JavaScript,解析和编译慢,体积大,一个简单的函数要写大量类型标注才能获得优化。
- 只有部分引擎认真优化了 asm.js,Chrome 的 V8 对 asm.js 的态度始终偏冷淡,跨浏览器性能不一致。
- 调试体验差,生成的代码人类几乎无法阅读,出错时只能对着几千行标注了
|0的 JS 发呆。
WebAssembly 从设计之初就解决了这些问题:二进制格式体积小、解析快,多浏览器统一支持,有独立的调试工具链。当 Wasm 在 2017 年落地四大浏览器后,asm.js 的存在理由只剩"向后兼容"这一条。
SpiderMonkey 的清理路线
SpiderMonkey 是 Mozilla 的 JS/Wasm 引擎,用 C++、Rust 和 JavaScript 编写,服务于 Firefox、Servo 等项目。此次调整的具体动作:
- Firefox 148:默认关闭 asm.js 优化路径。页面中的
"use asm"模块仍能正常执行,但引擎不再对其做专门的 AOT 编译,性能回落到普通 JS 水平。 - 后续版本:从 SpiderMonkey 源码中移除 asm.js 优化相关的全部代码分支,彻底删除这一能力。
这个节奏是合理的。先禁用优化、观察影响,再清代码。如果某个边缘场景仍然依赖 asm.js 的优化性能,开发者有缓冲期迁移到 Wasm。
从 asm.js 迁移到 WebAssembly:实操指南
如果你的项目仍在用 Emscripten 产出 asm.js 输出,迁移并不复杂。核心改动只有一个编译参数。
编译参数切换
Emscripten 早期默认输出 asm.js,后来默认输出 Wasm,但旧项目或旧版工具链可能仍在产出 asm.js。确认和切换方式:
# 查看当前 Emscripten 版本
emcc --version
# 旧方式:强制输出 asm.js(现在应该弃用)
emcc src/main.cpp -o output.js -s ASM_JS=1
# 新方式:输出 WebAssembly(emcc 1.38.6+ 之后已是默认)
emcc src/main.cpp -o output.js -s WASM=1
# 如果你的项目同时需要 asm.js 回退(兼容极老浏览器),可以开启双输出
# 但注意:asm.js 回退在 Firefox 148+ 将不再获得优化性能
emcc src/main.cpp -o output.js -s WASM=1 -s ASM_JS=2
-s ASM_JS=2 会同时生成 Wasm 和 asm.js 回退,浏览器优先加载 Wasm,只有在不支持 Wasm 时才走 asm.js。随着 Wasm 的浏览器覆盖率已超过 97%,这个回退基本可以去掉。
asm.js 手写模块 → Wasm 的对应写法
有些项目不是通过 Emscripten 编译,而是手写 asm.js 模块来加速计算。下面是一个典型 asm.js 模块和它迁移到 Wasm 的路径对比。
asm.js 版本(手动标注类型,依赖引擎识别 "use asm"):
function AsmModule(stdlib, foreign, buffer) {
"use asm";
var heap = new stdlib.Int32Array(buffer);
var imul = stdlib.Math.imul;
function compute(offset, n) {
offset = offset | 0;
n = n | 0;
var sum = 0;
var i = 0;
for (; (i | 0) < (n | 0); i = (i + 1) | 0) {
sum = (sum + imul(heap[(offset + i) | 0] | 0, 3) | 0) | 0;
}
return sum | 0;
}
return { compute: compute };
}
// 调用
var buffer = new ArrayBuffer(1024);
var mod = AsmModule(window, null, buffer);
mod.compute(0, 10);
这段代码的每个变量和运算都加了 |0 标注整数类型,写法笨重但引擎能快速编译。迁移到 Wasm 后,你不需要再写这些标注——用 WAT(WebAssembly Text Format)或直接用 C/Rust 编译即可。
WAT 版本(等价的 WebAssembly 文本格式):
(module
(memory (import "env") 1)
(func $compute (param $offset i32) (param $n i32) (result i32)
(local $sum i32)
(local $i i32)
(local.set $sum (i32.const 0))
(local.set $i (i32.const 0))
(block $break
(loop $loop
(br_if $break (i32.ge (local.get $i) (local.get $n)))
(local.set $sum
(i32.add
(local.get $sum)
(i32.mul
(i32.load (i32.add (local.get $offset) (local.get $i)))
(i32.const 3))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $loop)))
(local.get $sum))
(export "compute" (func $compute)))
实际项目中,手写 WAT 只用于极小的模块或学习。更常见的路径是用 C、C++ 或 Rust 编写逻辑,通过 Emscripten 或 wasm-pack 编译。
Rust → Wasm 示例(现代项目推荐路径):
// lib.rs
#[no_mangle]
pub extern "C" fn compute(heap: &[i32], offset: usize, n: usize) -> i32 {
heap[offset..offset + n]
.iter()
.map(|v| v * 3)
.sum()
}
# 用 wasm-pack 编译(面向 Web 场景)
wasm-pack build --target web
# 或用 Emscripten 编译 C 代码
emcc src/compute.c -O2 -o compute.wasm
JavaScript 端加载 Wasm 模块
async function loadAndRun() {
const response = await fetch('compute.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {
env: {
memory: new WebAssembly.Memory({ initial: 1 }), // 1 页 = 64KB
},
});
// 写入测试数据
const view = new Int32Array(instance.exports.memory.buffer);
for (let i = 0; i < 10; i++) view[i] = i + 1;
const result = instance.exports.compute(0, 10);
console.log('Result:', result); // 1*3 + 2*3 + ... + 10*3 = 165
}
loadAndRun();
这段代码可以直接复制到浏览器控制台或 HTML 页面中运行(前提是同目录下有编译好的 compute.wasm 文件)。
退场前该做的事
Firefox 148 禁用 asm.js 优化不会让现有页面崩溃——asm.js 模块仍以普通 JS 执行,只是慢了。但如果你有性能敏感的模块仍在用 asm.js,现在就该动手:
- 排查依赖:在项目中搜索
"use asm"字样和 Emscripten 旧版输出,确认哪些模块还在走 asm.js 路径。 - 升级工具链:Emscripten 升级到最新版,确认编译参数使用
-s WASM=1,去掉-s ASM_JS=1。 - 去掉 asm.js 回退:如果
-s ASM_JS=2还开着,关掉它。生成的.js壳文件会变小,加载更快。 - 测试性能:Wasm 在多数场景下比 asm.js 更快或持平,但少数边界情况(极小的计算模块)可能因 Wasm 的实例化开销略慢。跑基准测试确认。
- 关注 SpiderMonkey 源码动态:如果你维护的是嵌入 SpiderMonkey 的非浏览器项目(如 Servo),asm.js 代码移除后需要同步更新依赖。
asm.js 用十年时间证明了一件事:浏览器可以跑接近原生速度的代码。它用最笨的方式——在 JavaScript 的框架里硬塞低级语义——撬开了这扇门。WebAssembly 接过钥匙,把门正式打开了。老兵离场,门不会再关上。