为什么到 2025 年大多数开发者还不敢用 WebAssembly ?

  • 时间:2025-11-06 22:28 作者: 来源: 阅读:2
  • 扫一扫,手机访问
摘要:大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。1.WebAssembly 只能通过 JavaScript 间接操作 DOM使用 EM_JS 宏声明 JavaScript 函数WebAssembly 本身无法直接操作 DOM,由于 WebAssembly 目标是提供一种可移

大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

为什么到 2025 年大多数开发者还不敢用 WebAssembly ?

1.WebAssembly 只能通过 JavaScript 间接操作 DOM

使用 EM_JS 宏声明 JavaScript 函数

WebAssembly 本身无法直接操作 DOM,由于 WebAssembly 目标是提供一种可移植的、高效的二进制格式 以用于 Web 应用程序中的客户端和服务器端的计算密集型任务,而不能直接与 DOM 交互。不过,开发者可以通过 JavaScript 与 WebAssembly 之间的互操作来间接操作 DOM。

以下是一个简单的 C 语言代码示例 main.c,说明如何在 WebAssembly 中间接操作 DOM:

#include <emscripten.h>
// EM_JS 用于在 C 中将 JavaScript 声明为函数
EM_JS(void, js_update_dom, (const char *text), {
  const element = document.getElementById('wasm-output');
  element.innerHTML = UTF8ToString(text);
});
void update_dom(const char *text) {
    js_update_dom(text);
}
// main 是 C 程序的入口函数
// 注意:由 Emscripten 在 WebAssembly 模块初始化完成后自动从 JavaScript 侧调用
int main() {
    update_dom("Hello from WebAssembly!");
    return 0;
}

其中 EM_JS 是 JavaScript 库函数的便捷语法,其允许开发者在 C 代码中将 JavaScript 声明为函数,该函数可以像普通 C 函数一样被调用。例如,如果使用 Emscripten 编译并在浏览器中运行以下 C 程序,则会显示两个 alert:

EM_JS(void, two_alerts, (), {
  alert('hai');
  alert('bai');
});
int main() {
  two_alerts();
  return 0;
}

同时,参数也可以作为普通的 C 参数传递,并且在 JavaScript 代码中具有一样的名称,参数可以是 int32_t 类型或 double 类型。

EM_JS(void, take_args, (int x, float y), {
  console.log('I received:' + [x, y]);
});
int main() {
  take_args(100, 35.5);
  return 0;
}

因此,main.c 相当于定义一个 JavaScript 函数 js_update_dom 用于更新 DOM。该函数接收一个 const char * 类型的参数,然后通过 JavaScript 代码找到指定的 DOM 元素,并更新其内容。

接下来使用 Emscripten 编译 C 代码:

emcc main.c -s EXPORTED_FUNCTIONS='["_update_dom"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap","UTF8ToString"]' -o main.js

上面命令使用 Emscripten 编译 main.c 文件并生成 main.js 和 main.wasm 文件。通过设置 EXPORTED_FUNCTIONS 和
EXTRA_EXPORTED_RUNTIME_METHODS 选项,确保了需要的函数和方法可以在 JavaScript 中使用。

注意:Emscripten 使用 LLVM 和 Binaryen 将 C 和 C++ 编译为 WebAssembly,其输出可以在 Web、Node.js 和 wasm 运行时中运行。LLVM 表明一组编译器和工具链,可用于开发任何编程语言的前端和任何指令集架构的后端,而后者表明 WebAssembly 的优化器和编译器和工具链库。

然后创建一个 HTML 文件(index.html)来加载和运行上面输出的 WebAssembly 代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title> WebAssembly 和 DOM 互操作性示例 </title>
    // 只需要加载 Emscripten 生成的 .js 文件,即胶水代码
    // Emscripten 运行时会自动加载. wasm 文件
    <script async src="main.js"></script>
</head>
<body>
    <h1 id="wasm-output"></h1>
    <script>
        Module.onRuntimeInitialized = function() {
            // Emscripten 在 WebAssembly 模块初始化完成后自动从 JavaScript 侧调用
            Module._main();
        };
    </script>
</body>
</html>

HTML 文件加载了 main.js,并在
Module.onRuntimeInitialized 回调中调用 WebAssembly 生成的 _main 函数。此时,WebAssembly 代码就能间接操作 DOM 并将 <h1> 元素的内容设置为 "Hello from WebAssembly!"。运行此示例时,浏览器将显示 "Hello from WebAssembly!",这表明 WebAssembly 代码已成功间接操作 DOM。

值得一提的是,main.js 代码中会包括一系列的胶水代码(glue code),包括:

  • 加载和实例化 .wasm 文件:内部通过 fetch() 或 XHR 加载同名的 main.wasm 文件,并调用 WebAssembly.instantiate() 来实例化 WebAssembly 模块。
function findWasmBinary() {
  return locateFile("main.wasm");
}
async function instantiateAsync(binary, binaryFile, imports) {
  if (!binary && !isFileURI(binaryFile) && !ENVIRONMENT_IS_NODE) {
    try {
      var response = fetch(binaryFile, { credentials: "same-origin"});
      var instantiationResult = await WebAssembly.instantiateStreaming(
        response,
        imports
      );
      return instantiationResult;
    } catch (reason) {
      err(`wasm streaming compile failed: ${reason}`);
      err("falling back to ArrayBuffer instantiation");
    }
  }
  return instantiateArrayBuffer(binaryFile, imports);
}
wasmBinaryFile ??= findWasmBinary();
var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info);
// 加载 wasm
var exports = receiveInstantiationResult(result);
  • 提供运行时支持:WebAssembly 本身能力有限(列如不能直接调用 DOM API、不支持 malloc/free 等),Emscripten 的 JS 胶水代码提供了这些运行时功能,例如:内存管理(堆、栈)、C 标准库函数(如 printf, malloc)、字符串转换工具(如 UTF8ToString)、与 JavaScript 互操作的桥梁(如 cwrap, ccall)等等
  • 处理初始化回调:Module.onRuntimeInitialized 是 Emscripten 提供的一个钩子函数,当 WebAssembly 模块和运行时都准备就绪后会自动调用,此时可以安全地调用导出的 C 函数,例如 _main。

最后值得一提的是,通过 EM_JS 宏定义的 js_update_dom 函数不会被编译成 WebAssembly 指令,其会被嵌入到 Emscripten 生成的 main.js 胶水代码中,作为一个普通的 JS 函数存在。当 C 函数 js_update_dom 在 main.wasm 中被调用时,Emscripten 运行时会跳转到对应的 JS 函数执行 。

下面是胶水代码 main.js 中编译成的 js_update_dom 函数代码:

var wasmImports = {
  js_update_dom
};
function getWasmImports() {
  return {
    'env': wasmImports,
    'wasi_snapshot_preview1': wasmImports,
  }
}
function js_update_dom(text) {
  const element = document.getElementById('wasm-output');
  element.innerHTML = UTF8ToString(text);
}

这么做的意义在于:让 C/Wasm 代码 看起来 能操作 DOM,从而极大提升开发体验和代码组织性。即开发者可以在 C 逻辑中直接写 “更新页面” 语句,而不用把所有 DOM 逻辑都挪到 HTML/JS 文件里。

同时,虽然 WebAssembly 与 JS 之间的调用有开销,但 Emscripten 做了一系列优化,对于非高频操作(如按钮点击、结果展示)完全可以接受。

使用 emscripten_set_click_callback 声明回调函数

除了使用 EM_JS 来定义一个 javascript 函数,开发者还可以通过
emscripten_set_click_callback 函数来注册一个回调函数,来响应 HTML 元素的点击事件,例如下面的例子:

#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
// 相当于告知编译器和链接器保留该符号并将其导出
void handle_click() {
    // 处理 DOM
}
int main() {
    emscripten_set_click_callback("myButton", NULL, 1, handle_click);
    return 0;
}

在上面的例子中,handle_click 函数是一个在 Wasm 模块中定义的函数,其通过 EMSCRIPTEN_KEEPALIVE 宏来确保在编译时不会被优化掉。

然后,该函数使用
emscripten_set_click_callback 函数来将自己注册为名为 myButton 的 HTML 元素的单击事件的回调函数。在 JavaScript 中,可以通过以下方式来加载并调用 Wasm 模块中导出的函数:

const response = await fetch("my_module.wasm");
const buffer = await response.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
const handle_click = module.instance.exports.handle_click;
// 获取导出的 handle_click 函数
document.getElementById("myButton").addEventListener("click", handle_click);

在上面的例子中,第一通过 fetch 函数加载 Wasm 模块,然后通过 WebAssembly.instantiate 函数将实则例化。最后,可以通过 module.instance.exports 对象访问 Wasm 模块中导出的函数,并将其作为 HTML 元素事件的回调函数注册。

2.WebAssembly 内存管理危机与 WasmGC 救场

垃圾回收是指程序尝试回收由程序分配但不再引用的内存,即垃圾。而实现垃圾回收的方法有许多,例如:引用计数,其目标是统计内存中对象的引用数量。

// 循环引用导致潜在的内存泄漏
function f() {
  const x = {};
  const y = {};
  x.a = y; // x 引用 y
  y.a = x; // y 引用 x
  return "azerty";
}
f();

而编程语言分为两类,垃圾回收型编程语言 和 需要手动管理内存的编程语言。一般而言,高级编程语言更有可能将垃圾回收作为一项标准功能。

  • 垃圾回收型语言: JavaScript、Kotlin、PHP 或 Java 等
  • 需要手动内存管理的编程语言: C、C++ 或 Rust 等

由于编程语言本身是由其他编程语言实现的,例如:PHP 运行时主要采用 C 语言实现。如果开发者想要将 PHP 等语言编译为 Wasm,一般需要编译所有部分,例如 语言的解析器、库支持、垃圾回收 和其他关键组件。

在 Chrome 中,JavaScript 和 Wasm 在 Google 开源的 JavaScript 引擎 V8 中运行。V8 已经有垃圾回收器,这意味着如果开发者使用的是编译为 Wasm 的 PHP,最终会将 PHP 的垃圾回收器实现移植到已具有垃圾回收器的浏览器,从而造成浪费。

这正是 WasmGC 的用武之地!WasmGC 是 WebAssembly 社区组的一项提案。

由于当前的 Wasm MVP 实现只能处理 线性内存 中的数字,即整数和浮点数,而随着引用类型提案的发布,Wasm 还可以保存外部引用。WasmGC 目前添加了 结构体和数组堆类型,这意味着支持非线性内存分配。每个 WasmGC 对象 都具有固定的类型和结构,这使得虚拟机可以轻松生成高效的代码来访问其字段,而不会像 JavaScript 等动态语言那样存在反优化的风险。

WasmGC 提案通过结构体和数组堆类型为 WebAssembly 添加了对高级别托管语言的高效支持,从而 使以 Wasm 为目标的语言编译器能够与宿主虚拟机中的垃圾回收器集成。这意味着使用 WasmGC 时,将编程语言移植到 Wasm 意味着编程语言的垃圾回收器不再需要成为移植的一部分,而是可以使用现有的垃圾回收器。

为什么到 2025 年大多数开发者还不敢用 WebAssembly ?

由于可以在编译为 WasmGC 后进行常规优化,因此 Wasm-to-Wasm 优化器可以为所有 WasmGC 编译器工具链提供支持。为此,V8 团队在 Binaryen 中引入了 WasmGC,所有工具链都可以将其用作 wasm-opt 命令行工具。

目前 Chrome 已经会默认启用 WebAssembly 垃圾回收 (WasmGC)。

3.WebAssembly 还有什么缺点

前面小结讲述了 WebAssembly 只能通过 JavaScript 来间接操作 DOM,实则除此之外,WebAssembly 实则还是不少难以跨越的难点,例如:

  • 调试复杂:与 JavaScript 相比,调试 WebAssembly 代码更加困难。缘由是 Wasm 代码一般是从 C、C++ 或 Rust 等语言编译而来,导致二进制格式难以理解。而调试工具目前还不够先进,因此可能需要花费更多时间和精力来识别代码中的错误。
  • 生态受限:尽管 WebAssembly 生态正在积极发展,但其依旧不如 JavaScript 生态广泛。而某些库和框架可能不支持 Wasm,或者需要额外的集成工作。
  • 内存管理:Wasm 的内存管理模型带来诸多挑战,尤其是对于内存密集型应用程序而言。分配大内存空间在所有浏览器上可能并不可靠,尤其是在移动设备上。 JavaScript 有一个垃圾收集器 (GC),但 WebAssembly 目前并不了解该 GC。由于 wasm 代码使用 JS 代码与 DOM 交互,因此这可能是内存泄漏的根源。

参考资料

https://juejin.cn/post/7219738264851628090

https://developer.mozilla.org/en-US/docs/WebAssembly/Guides/Concepts

https://emcc.zcopy.site/docs/getting_started/downloads/

https://blog.csdn.net/u011520181/article/details/134124828

https://www.scrumlaunch.com/blog/webassembly-in-2025-why-use-it-in-modern-projects

https://web.dev/blog/wasmgc-wasm-tail-call-optimizations-baseline

https://developer.chrome.com/blog/wasmgc/

https://emscripten.org/docs/api_reference/emscripten.h.html

  • 全部评论(0)
最新发布的资讯信息
【系统环境|】web前端培训:6个常用的前端开发构建工具(2025-11-06 22:33)
【系统环境|】现代包管理器pnpm 、npm、yarn?(2025-11-06 22:32)
【系统环境|】一款无需写任何代码即可一键生成前后端代码的开源工具(2025-11-06 22:32)
【系统环境|】提示工程架构师教你借助Agentic AI提升社交媒体用户留存率(2025-11-06 22:31)
【系统环境|】电子元器件-逻辑器件篇:逻辑电平、CMOS逻辑、手册解读、逻辑电平转换,应用注意事项(2025-11-06 22:31)
【系统环境|】Linux基础-包管理工具yum和apt对比(2025-11-06 22:30)
【系统环境|】RPM包离线下载方法(2025-11-06 22:30)
【系统环境|】红帽linux系统与UOS命令对比(2025-11-06 22:29)
【系统环境|】从 MIB 到告警:手把手教你用 Prometheus 监控交换机端口(2025-11-06 22:29)
【系统环境|】GitLab 13.12发布,安全性、可用性和管道管理加强(2025-11-06 22:28)
手机二维码手机访问领取大礼包
返回顶部