什么是 WebAssembly
WebAssembly(简称 Wasm)是一种低级的二进制指令格式,让 C/C++/Rust 等语言编译出来的程序能直接在浏览器里跑。它不是要取代 JavaScript,而是补上 JS 不擅长的那一块:计算密集型任务。浏览器把它当成一种新的、可高度优化的运行目标。
Wasm 不是"另一种语言",而是让现有高性能语言能进浏览器的一条通道。
- 二进制体积小、加载与解析快。
- 接近原生计算性能远高于 JS。
- 安全隔离在沙箱里运行,不碰 DOM。
- 互补重计算交给 Wasm,界面仍用 JS。
它为什么比 JS 快
JS 是动态类型语言,运行时要做大量类型推断与优化;Wasm 是接近机器码的二进制,加载即编译,没有解释的开销,执行效率接近原生。简单说:JS 像边翻译边跑,Wasm 像提前编译好直接跑。对海量循环、图像处理、加密这类纯计算任务,差距可以达到几倍到几十倍。
要理解这个差距的来源,关键在类型。JS 里一个变量今天装数字、明天可能装字符串,引擎要边跑边猜;Wasm 模块在编译时就把每一处的类型、内存布局定死了,运行时直接按机器指令执行,没有解释和 JIT 预热的开销。所以同样一段浮点密集的循环,Wasm 往往能跑到接近原生的速度,而 JS 要花很久才能被引擎优化到位。
典型应用场景
适合用 Wasm 的场景:大型在线编辑器(如把 Photoshop 搬到网页)、视频编解码、图像滤镜、科学计算、游戏引擎、把已有的 C/C++ 库移植到浏览器。判断标准很简单:如果这段逻辑用 JS 算起来明显卡顿,就该考虑 Wasm。纯 DOM 操作、页面逻辑,老老实实用 JS。
在浏览器里加载与调用
浏览器原生支持 .wasm 模块,用 WebAssembly.instantiate 加载,导出的函数就能直接调用。下面是最简化的加载流程。
// 加载一个编译好的 wasm 模块并调用导出函数
WebAssembly.instantiateStreaming(fetch('add.wasm'))
.then(({ instance }) => {
const sum = instance.exports.add(3, 4);
console.log('结果是', sum); // 7
});
实际项目里一般用 Rust/Go 工具链把代码编译成 wasm,再由 JS 胶水层调用,手写 wasm 很少见。
JS 与 Wasm 如何交换数据
Wasm 有自己的线性内存(一块字节缓冲区),它不能直接碰 DOM。复杂数据(比如一段文字、一张图片)要在 JS 侧把数据写进这块内存,再让 Wasm 读取;返回结果也是一段二进制,由 JS 解释。这种边界拷贝的成本,是选型时要权衡的——数据反复跨边界传,优势会被抵消。
因此经验是:让 Wasm 一次吞进尽可能多的数据、在内部把计算全部做完,再一次性把结果交回 JS,而不是每算一步就来回传一次。图像处理就是典型——把整张图片的像素数组一次性拷给 Wasm,在里面跑完滤镜算法,再把结果数组拷回来显示,这样跨边界的次数最少,收益最大。
选型:什么时候才用
记住一句话:Wasm 是性能补丁,不是默认选择。先写 JS,用性能工具定位到确实是计算瓶颈的那段,再把那一小块交给 Wasm。为了用而用、整个项目重写,往往得不偿失——调试难、体积大、团队门槛高。
落地的正确姿势是渐进引入:先用 JS 把产品做出来,遇到确实拖慢体验的计算瓶颈,再把那一小段抽成 Wasm 模块,其余代码照旧。这样既拿到了关键路径的性能,又不破坏已有的工程习惯和调试链路。把它当成给重计算开的一个外挂通道,而不是要推翻重来的一次技术站队。
小结
入门要点回顾:
- Wasm 是给浏览器用的二进制高性能运行格式。
- 计算密集任务才用它,界面逻辑仍用 JS。
- 通过 instantiate 加载,调用导出函数。
- 注意 JS 与 Wasm 之间数据拷贝的成本。
- 先测性能瓶颈,再决定要不要引入。