asm.js 正式退场:Firefox 148 默认禁用,WebAssembly 完成十年交接

2026-05-21 22 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

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 等项目。此次调整的具体动作:

  1. Firefox 148:默认关闭 asm.js 优化路径。页面中的 "use asm" 模块仍能正常执行,但引擎不再对其做专门的 AOT 编译,性能回落到普通 JS 水平。
  2. 后续版本:从 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,现在就该动手:

  1. 排查依赖:在项目中搜索 "use asm" 字样和 Emscripten 旧版输出,确认哪些模块还在走 asm.js 路径。
  2. 升级工具链:Emscripten 升级到最新版,确认编译参数使用 -s WASM=1,去掉 -s ASM_JS=1
  3. 去掉 asm.js 回退:如果 -s ASM_JS=2 还开着,关掉它。生成的 .js 壳文件会变小,加载更快。
  4. 测试性能:Wasm 在多数场景下比 asm.js 更快或持平,但少数边界情况(极小的计算模块)可能因 Wasm 的实例化开销略慢。跑基准测试确认。
  5. 关注 SpiderMonkey 源码动态:如果你维护的是嵌入 SpiderMonkey 的非浏览器项目(如 Servo),asm.js 代码移除后需要同步更新依赖。

asm.js 用十年时间证明了一件事:浏览器可以跑接近原生速度的代码。它用最笨的方式——在 JavaScript 的框架里硬塞低级语义——撬开了这扇门。WebAssembly 接过钥匙,把门正式打开了。老兵离场,门不会再关上。


相关推荐