xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • WebAssembly DOM

WebAssembly何时获得DOM支持?

WebAssembly(Wasm)是否已准备好用于生产环境的web应用?尽管这些应用需要与网页及其API(如DOM)集成。问题的答案既可以是“Wasm可能永远不会获得直接DOM访问”,也可以是“是的,Wasm已准备好用于各种web集成场景,因为它从第一天起就支持通过间接方式调用DOM”。让我解释一下。

Wasm从设计之初就与JavaScript严格分离。与asm.js(https://developer.mozilla.org)不同——后者不可避免地嵌入JavaScript中——核心Wasm字节码格式完全脱离了JavaScript的“遗留”特性。同时,正如其名称所示,Wasm是web平台的一部分。那么,为什么DOM等web API仍绑定到JavaScript?为什么没有新的Wasm版本?答案在于:现有JavaScript API已足以从Wasm调用,无需新版本。在浏览器或其他JavaScript环境中,Wasm包含各种JavaScript API,允许编译器生成的胶水代码无缝访问任何JavaScript代码。Wasm正逐步进化,减少构建工具链生成的代码量(特别是JavaScript),以解决真实web应用在Wasm中性能慢和体积大的问题。W3C Wasm社区组(CG)将持续推动Wasm与JavaScript的集成深化。如果未来证明性能提升和代码大小优化能证明其复杂性,可能会添加直接从Wasm调用web API的机制,但这需要巨大努力。另一方面,少量JavaScript胶水代码可能不是瓶颈,尤其当存在其他开销源时。

将Wasm粘合到JavaScript

要理解Wasm和JavaScript如何协作,请看一个改编自MDN文档的示例,展示Wasm如何调用console.log。Wasm的主要格式是二进制字节码,但其规范也定义了文本格式WAT(Wasm文本),便于理解:

(module
  (func $log (import "myConsole" "myLog") (param i32))  ; 导入名为myLog的函数,参数类型为i32
  (func (export "log42")  ; 导出名为log42的函数
    i32.const 42  ; 将常量42压入栈
    call $log))  ; 调用导入的log函数

Wasm是模块化语言:每个模块声明导入和导出。实例化模块时提供导入,操作返回导出。此简单模块从myConsole导入myLog函数,并导出log42函数(调用导入函数并传入42)。

JavaScript代码可以引用全局对象调用任何web API,但Wasm只能访问给定的内容。因此,console.log通过模块导入传递到Wasm:

const importObject = {  // 创建导入对象,提供JavaScript功能给Wasm
  myConsole: {
    myLog(arg) {
      console.log(arg);  // 实际调用JavaScript的console.log
    }
  }
};
const { instance } = await WebAssembly.instantiateStreaming(  // 异步实例化Wasm模块
  fetch("simple.wasm"),  // 加载Wasm文件
  importObject  // 传入导入对象
);
instance.exports.log42();  // 调用Wasm导出的函数

构建工具链输出网站Wasm时,会输出Wasm和JavaScript的组合,后者负责加载模块并与JavaScript能力(如myConsole)链接。

用平坦内存建模一切

如德国数学家Leopold Kronecker所言,上帝创造了i32、i64、f32和f64;其余都是人为之作。Wasm始于简单内置类型系统:栈值和函数参数仅支持字节可寻址数值类型(i32、i64、f32、f64)。然而,这使Wasm字节码能灵活调用JavaScript API,即使它们接受对象参数(而非数字)。方法如下:在JavaScript中维护一个数组,存储所有Wasm程序需引用的JavaScript对象。导出到Wasm的模块被包装:当函数接受对象参数并返回对象时,包装器接受并返回数组索引(为返回值分配新条目)。Wasm代码通过导入的JavaScript函数显式释放引用(管理空闲列表)。最终产物是JavaScript和Wasm的捆绑包,而非纯Wasm。

这种最小设计借鉴了asm.js的成功经验,并吸取了Chrome NaCl(Native Client)的教训——后者引入新I/O系统Pepper但未获跨浏览器支持(NaCl已弃用)。Wasm的初衷是让更多程序在web上高效运行,而非消除所有JavaScript。编译到Wasm的程序核心可能是优化关键,尤其在JavaScript中可能慢或大时。用Wasm而非JavaScript传输核心能提升应用性能,边缘少量JavaScript不影响此优势。核心在于Wasm由编译器工具链构建,能建立约定并生成跨边界匹配代码。主流工具链包括C/C++(https://emscripten.org)、Rust(https://rustwasm.github.io)、Kotlin(https://kotlinlang.org)和Java(https://developer.fermion.com),它们均以类似方式桥接web API,应用开发者无需操心。

先恶化后改善

在混合JavaScript/Wasm设计中,一切均可建模,但会增加间接性。例如:

  • 异常处理:调用导入JavaScript函数抛出异常(不难),但建模try/catch块(难)需导出内部块函数到JavaScript,在JavaScript中执行try/catch,再导出组合函数回Wasm。
  • 阻塞I/O:许多需编译到Wasm的程序有同步阻塞行为,但web平台基于非阻塞Promises/回调。选项:在web worker中运行Wasm(可阻塞),或用JavaScript SharedArrayBuffer支持的Wasm内存与主线程通信;或通过continuation-passing风格显式表示调用栈以挂起/恢复计算。
  • 垃圾回收值:语言编译到Wasm时,选项:1)将自有GC编译到Wasm(操作平坦内存),需维护“影子栈”追踪根(因Wasm不让用户GC访问栈);2)调用JavaScript分配值并用普通JavaScript对象操作。

用Wasm最小起点表达一切可行,但Wasm的目标是通过减少间接性加速应用。当前方案反而增加间接性:例如C++移植到Wasm时异常处理方案导致显著减速。

改善之路

关键是优先“什么让程序实际更好”,而非“什么让一切在Wasm中完成”,聚焦性能或代码大小的痛点。策略是增量添加能力,保持整体一致性。有时实现可透明提升性能:

  • 优化Wasm/JavaScript间调用(https://hacks.mozilla.org),包括构建生成的代码。
  • 引入多层JIT编译器(https://v8.dev、https://spidermonkey.dev),优先优化高频代码。
  • 在优化JIT中内联Wasm/JavaScript间调用(https://chromium.googlesource.com)。

但Wasm需添加新能力消除某些开销:

  • 异常:原生支持Wasm异常抛出和try/catch(https://github.com)。
  • 阻塞操作:Wasm原生暂停执行(https://v8.dev)以等待JavaScript Promise。
  • 垃圾回收值:基于共享JavaScript堆的改进:
    • 栈和全局引用JavaScript值(https://github.com),无需额外数组。
    • 分配和访问GC结构/数组(https://github.com),避免JavaScript开销。
    • 直接操作JavaScript字符串,免堆间复制。
    • JavaScript终结器,Wasm程序在值未用时清理线性内存(https://developer.mozilla.org)。

好消息:Wasm已添加这些特性(https://web.dev),至少在最先进实现中。虽未消除胶水代码,但显著提升了可能交互DOM的代码(如现代垃圾回收语言构建的UI)性能。

但直接DOM访问呢?

Wasm组件模型(Component Model)被视为直接访问web API的可能路径。早期草案称“Web IDL绑定”(https://github.com)。Web IDL(https://webidl.spec.whatwg.org)基于OMG IDL(https://omg.org),源自CORBA,但分叉以精确指定web API在JavaScript中的行为。Web IDL本是跨语言机制(支持Java等),API实现多用C++/Rust。但Web IDL与JavaScript难分离:它已弃用Java支持,转向迭代器、Promises等“JavaScript化”API。所有现代浏览器基于Web IDL生成API代码确保一致性。

添加Wasm绑定挑战:代码生成器脚本庞大(数千行)、难改;浏览器源中Web IDL与规范有细微差异;非所有web API使用Web IDL(如JavaScript标准库未定义于Web IDL)。因此,web API(包括DOM)本质上是JavaScript原生。

组件模型可能暴露web API为Wasm模块(类似JavaScript字符串支持)。但用组件模型更高效:不同语言内存布局不同(如字符串在平坦vs. GC内存),组件模型避免额外拷贝。Jco项目(https://bytecodealliance.github.io)原型了通过Web IDL和组件模型的web API导入。但这能否提升运行时性能或减小应用大小?胶水代码减少可能带来大小优化,但收益需更多原型验证。目前web社区未优先此大型项目,浏览器厂商无积极动作。计划让服务器主导的组件模型发展,再看是否有用成果。Jco开发中的可能未来是组件模型作为工具链约定(非浏览器内置),工具链用共享组件访问web API/DOM而非自建绑定,但这仍通过JavaScript。

进化Wasm字节码格式

Wasm由标准委员会(W3C Wasm CG)开发,专家设计、辩论、原型化和文档化新特性。CG开放(https://w3.org),遵循轻量投票(https://github.com)达成粗略共识,采用六阶段流程(https://github.com)交织设计、原型和共识。输入多方重要:

  • Wasm引擎维护者(确保可实施)。
  • 构建工具链维护者(确保可行有用)。
  • 应用开发者(评估整体影响)。
  • 学术研究者(应用研究、形式定义)。
    贡献方式:GitHub issue反馈(https://github.com),参加CG会议(https://github.com)。

Wasm完整了吗?

仍可减少JavaScript/Wasm边界开销:

  • 运行时类型元数据:Wasm GC对象分配时,引擎需对象头指针指向类型级元数据(用于GC追踪和边界类型检查)。提议合并元数据为单指针(https://github.com),避免额外内存字和包装对象。
  • 类型检查优化:JavaScript API常需参数类型运行时检查(如DOM方法)。Wasm类型系统若模型对象为泛型指针,则需运行时检查。提议类型导入(从模块导入类型),可略减少调用DOM开销(https://github.com),但因类型检查快未优先。
    其他提案如内存控制(null指针解引用抛异常提升安全,https://github.com、https://wingolog.org)。

每个特性(包括直接DOM/web API访问)由浏览器厂商基于实际性能/大小改进决定实施。否则,Wasm集成web应用的工具已备且成功应用。

Wasm的定位

对软件开发者,相关问题是“能否将C#/Go/Python库/应用构建到网站并以好性能运行?”,而非“能否写纯Wasm直接访问DOM且不碰JavaScript?”。无人愿直接写字节码(即使添加DOM访问工具)。理想中,Wasm应是开发者无需考虑的细节。当前JavaScript“直接工作”(web API和工具针对它设计),但Wasm的论点(且必须是)是构建步骤可行。可预见未来,web部署Wasm的构建输出将含JavaScript和Wasm。

Daniel Ehrenberg是Bloomberg JavaScript基础设施与工具团队的软件工程师,Ecma International主席,贡献于Ecma TC39(JavaScript标准委员会)。曾在Igalia和Google V8团队涉足Wasm和web标准。

© 2025 作者/所有者。出版权许可给ACM。原载于Queue vol. 23, no. 3。

总结

本文深入探讨了WebAssembly(Wasm)在web应用中的DOM支持问题,核心论点是:Wasm目前没有直接DOM访问,但通过JavaScript胶水代码已实现高效集成,且性能优化持续进行。主要内容总结如下:

  • Wasm设计原则:Wasm从设计之初与JavaScript严格分离,核心字节码独立于JavaScript“遗留”特性,但作为web平台一部分,需通过JavaScript胶水代码调用DOM等web API。这种间接方式(如模块导入/导出机制)从Wasm诞生起就支持,允许编译器工具链(如Emscripten、RustWasm)生成混合Wasm/JavaScript输出,开发者无需手动处理集成。
  • 当前集成机制:Wasm通过平坦内存(i32/i64/f32/f64类型)建模JavaScript对象,例如维护JavaScript引用数组,用索引传递对象参数。这借鉴了asm.js经验,但增加了间接性(如异常处理需JavaScript try/catch包装),导致性能问题(如C++移植的减速)。构建工具链自动处理这些细节,使应用核心逻辑在Wasm中运行优化。
  • 性能优化进展:Wasm社区(W3C CG)增量添加特性以减少开销:
    • 原生异常支持(try/catch in Wasm)和阻塞操作(暂停执行等待Promise)提升关键场景性能。
    • 垃圾回收改进:共享JavaScript堆、直接操作字符串、终结器机制等,显著加速UI相关代码(现代GC语言构建)。
    • 引擎优化:多层JIT、调用内联等透明提升性能。
      这些特性已在先进实现中部署(如Chrome V8、Firefox SpiderMonkey),但未消除胶水代码。
  • 直接DOM访问的挑战:Web API本质是JavaScript原生,Web IDL绑定提案复杂:生成器脚本难改、浏览器实现差异、非所有API使用Web IDL。Wasm组件模型被视为潜在解决方案(如Jco原型),允许高效内存布局处理,但需巨大努力且浏览器厂商无积极动作。可能未来是工具链约定(非浏览器内置),但仍通过JavaScript。
  • 标准过程:Wasm由W3C CG开发,开放社区贡献,六阶段流程确保设计-原型-共识平衡。输入多方(引擎维护者、工具链开发者、应用开发者、研究者)关键,优先特性基于实际收益(如类型元数据合并提案减少内存开销)。
  • 开发者视角:Wasm定位为优化工具而非纯解决方案。核心价值是将非JavaScript语言(C#/Go等)库集成到web,以好性能运行。构建输出必然包含JavaScript胶水代码,但性能瓶颈逐渐缓解。Wasm的“构建步骤”理念是可行的,且生态系统成熟。

总之,Wasm DOM支持问题核心在于平衡性能、复杂性和实用性:直接访问未实现(因设计复杂性和web API的JavaScript原生性),但当前间接方式通过持续优化已胜任生产环境,未来组件模型可能提供更优雅方案。

最后更新: 2025/8/26 10:07