#AI#TypeScript

WebAssembly:在浏览器跑接近原生的性能

JS 太慢的场景(图像/视频/3D/加密/大计算),WebAssembly 让浏览器跑 C/Rust 编译的代码,接近原生速度。本文讲清 WASM 是什么、和 JS 的关系(互补不替代)、怎么用(Rust 编译 wasm + JS 调用)、适合什么(WASM 的甜区)不适合什么。

本文要点

  • 主题聚焦:AI、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年3月16日·更新于 2026年7月23日

一个很现实的场景:你用 JS 写一个图片处理工具,对一张 4000×3000 的图做高斯模糊,几千万像素扫一遍,主线程卡死 2 秒,用户以为浏览器挂了。你想着"是不是算法写得不好",其实就算写得再好,V8 的 JIT 也追不上同样算法的 C++ 原生实现——不是 JS 差,是它天生就不是为这种 CPU 密集计算设计的。

WebAssembly(WASM)就是来解决这个问题的:把 C/C++/Rust 编译成浏览器能直接跑的字节码,在那些 JS 撑不住的场景里,拿到接近原生的性能。但它不是"JS 杀手",也不该到处用——这篇文章讲清 WASM 是什么、怎么用、什么时候用。

一、WASM 到底是什么

一句话:WASM 是一种可移植、体积小、加载快、能在浏览器里以接近原生速度执行的低层字节码格式。

它的执行链路是这样的:

  1. 你用 C、C++、Rust、Go、AssemblyScript(TS 的子集)等语言写逻辑。
  2. 用对应的工具链把源码编译成 .wasm 文件——这是一个紧凑的二进制字节码,不是给 CPU 直接跑的机器码,而是给浏览器里的 WASM 虚拟机跑的。
  3. 浏览器把 .wasm 解码、校验、编译成当前平台的机器码(AOT,而不是 JS 的 JIT),然后执行。

关键差异在这里:JS 是"边跑边优化",要先解释、热点才 JIT、可能还会反优化(de-optimize);WASM 是"加载时就编译完",跑的时候没有解释器开销,类型也是静态的,不需要运行时猜测。所以在 CPU 密集场景,WASM 比 JS 快几倍到几十倍是完全正常的。

但 WASM 不是一门"新语言"——你不会手写 WASM 字节码(就像你不会手写 x86 汇编)。它是编译目标,你写的是上层语言,产物是 .wasm。理解这一点很重要,因为它决定了 WASM 的工程化方式:不是"换个语言写前端",而是"把后端语言的能力搬到浏览器"。

二、和 JS 的关系:互补,不是替代

网上很多标题党说"WASM 要取代 JS",这是误解。真实的关系是:JS 做编排,WASM 做重计算,两者通过函数调用协作。

原因在于 WASM 的设计取舍。WASM 被刻意设计成最小运行时:它能做整数浮点运算、内存读写、函数调用,但它不能直接操作 DOM、不能发 fetch、不能访问 localStorage、不能监听事件。这些"Web API"只能通过 JS 来调,WASM 要操作 DOM,必须通过 JS 桥。

所以典型的协作模式是:

  • JS 负责:UI、事件处理、DOM 操作、网络请求、状态管理——这些是 JS 的舒适区,V8 优化得好,生态最全。
  • WASM 负责:被 JS 调用的、计算密集的纯函数——图像处理、音视频编解码、加密、压缩、物理引擎、大数组运算。
  • 桥接:JS 通过 WebAssembly.instantiate 加载 .wasm,拿到导出的函数像普通 JS 函数一样调;WASM 内部要操作 DOM 时,通过 import 的 JS 函数回调。

一句话:JS 是 Web 的"灵魂语言",WASM 是 JS 的"重型外挂"。Figma、Photoshop Web、Google Earth 这种重度应用都是这么搭的——UI 用 Web 技术画,核心算法跑在 WASM 里。

三、怎么用:Rust → WASM → JS 全流程

Rust 是目前写 WASM 体验最好的语言(零成本抽象 + 没有运行时 GC,体积小、性能可预测)。下面是一个完整的最小例子。

第一步:装工具链

bash
rustup target add wasm32-unknown-unknown
cargo install wasm-pack      # 一站式打包工具

wasm-pack 是 Rust 官方推荐的工具,它帮你把 .rs 编译成 .wasm,同时生成 JS 胶水和 TypeScript 类型声明。

第二步:写 Rust 逻辑

rust
// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
    // pixels 是 RGBA 字节流,每 4 字节一个像素
    for chunk in pixels.chunks_exact_mut(4) {
        let r = chunk[0] as u32;
        let g = chunk[1] as u32;
        let b = chunk[2] as u32;
        // ITU-R BT.601 加权平均
        let gray = (r * 299 + g * 587 + b * 114) / 1000;
        chunk[0] = gray as u8;
        chunk[1] = gray as u8;
        chunk[2] = gray as u8;
        // chunk[3] 是 alpha,不动
    }
}

#[wasm_bindgen] 是关键宏,它告诉 wasm-bindgen"把这个函数导出给 JS 用",并自动处理 JS↔WASM 之间的类型转换。

第三步:编译

bash
wasm-pack build --target web

产物在 pkg/ 目录下:xxx_bg.wasm(字节码)、xxx.js(胶水,包含加载和类型转换)、xxx.d.ts(TS 类型)。

第四步:JS 调用

ts
import init, { grayscale } from './pkg/my_grayscale.js';

await init(); // 加载并实例化 .wasm

const canvas = document.querySelector('canvas')!;
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// 把像素数据传给 WASM,就地处理
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);

注意这里 imageData.data 是个 Uint8ClampedArray,wasm-bindgen 会把它直接以共享内存的方式传给 Rust——不是逐字节复制。这点对性能至关重要,下面单独讲。

这就是 Rust→WASM→JS 的完整闭环。换成 C++ 也类似,只是工具链换成 Emscripten(emcc),JS 端换成一个稍重的胶水文件。

四、WASM 的优势在哪

为什么折腾这么一套编译流程值得?

  1. 性能:CPU 密集场景,WASM 通常比 JS 快 2× 到 30×。差距最大的地方是"确定类型的大循环"——JS 的 JIT 要在运行时猜类型,猜错就 de-optimize;WASM 类型静态,直接编译成 SSE/AVX 向量化指令也不奇怪。
  2. 可移植:你能把已有的 C/C++ 库直接搬到 Web。FFmpeg、SQLite、PDF.js、libpng、OpenCV……这些工业级实现不用重写,编译成 WASM 就能用。这是 WASM 最被低估的价值——复用 40 年的 C 库生态。
  3. 安全:WASM 跑在沙箱里,默认没有文件系统、网络、进程访问权限,只能通过显式 import 的 JS 函数和外界交互。从陌生源加载的 .wasm 也跑不出沙箱。
  4. 体积/启动:二进制比 JS 源码紧凑(同等逻辑通常小 30%–50%),而且可以流式编译(下载到一半就开始编译),冷启动比同等 JS 应用快。
  5. 确定性:没有 JIT 的 de-optimize 抖动,长任务的 P99 延迟更稳定——对编辑器、游戏这种"每帧都要准时"的应用是刚需。

五、WASM 的甜区:这些场景才值得上

不是所有 Web 代码都该用 WASM。下面这些场景才是它真正的舒适区:

  • 图像/视频处理:滤镜、编解码、视频剪辑。典型如 Photoshop Web 版,核心图像算法跑在 WASM 里。
  • 3D / 游戏引擎:Unity、Unreal 都能导出 WASM,Web 版游戏物理/渲染走 WASM,WebGL/WebGPU 做底层绘制。
  • 加密 / 压缩:AES、SHA、zstd、gzip 的 C 实现,在 WASM 里比 JS 实现快好几倍,适合做端到端加密、大文件压缩、密码学计算。
  • 大型数据计算:前端做本地数据分析(Excel 重算、DataFrame 转换、CSV 解析百万行),WASM 比 JS 不卡 UI。
  • 复用 C/C++ 库到 Web:FFmpeg(Web 版剪辑器)、SQLite(sql.js)、PDF 渲染、字体解析——这些用 JS 重写既慢又不现实。

一句话总结甜区:计算量大、循环深、类型确定的纯计算。这种活,WASM 才能把 JS 按在地上摩擦。

六、这些场景别用 WASM,适得其反

反过来,以下场景上 WASM 是负收益,我见过太多团队"为了用 WASM 而用",最后不得不拆掉:

  1. DOM 操作:WASM 不能直接碰 DOM,必须过 JS 桥。一个 document.createElement 在 WASM 里要 import 一个 JS 函数,跨边界一次几十微秒——批量操作 DOM 时,JS 直接秒杀 WASM。框架层面有 wasm-dom 这种实验性东西,但不要拿生产去赌。
  2. 简单逻辑:isValidEmail、calculateTotal 这种轻量函数,JS 引擎秒级编译,而 WASM 有加载、解码、链接成本——首次调用前要等几百毫秒。编译开销 > 收益时,纯属浪费。
  3. I/O 密集:大量 fetch、await、事件回调的任务,瓶颈在网络或调度,不在 CPU。WASM 没有 fetch API,反而要走 JS 桥,慢上加慢。
  4. 字符串 / 对象密集交互:JS 和 WASM 之间默认只能传数字和 ArrayBuffer。传字符串、对象都要序列化或拷贝,频繁跨边界会把 WASM 的算力收益吃光。

判断标准:如果一段逻辑的"输入 + 输出"远小于它"内部计算的量"(典型的图像处理:输入一张图,输出一张图,中间扫几千万像素),那就是 WASM 甜区;反过来,如果逻辑本身很薄、要频繁和 DOM/网络交互,JS 写就完事了。

七、上 WASM 要付的代价

把 WASM 引入项目不是免费的,以下三件事是真实成本:

1. 文件体积和加载。一个有意义的 WASM 模块通常几百 KB 到几 MB(FFmpeg 这种能到 20MB+)。它走的是额外的网络请求,而且要先下载、解码、编译才能用。优化手段:代码分割(按需 .wasm)、用 WebAssembly.instantiateStreaming 流式编译、开启 HTTP 缓存、用 Brotli 压缩 .wasm(二进制也能压)。

2. JS↔WASM 数据传递开销。这是新手最容易踩的坑。WASM 默认只能访问自己的线性内存(一块 ArrayBuffer),JS 要把数据传进去,要么复制(wasm-bindgen 对小数据会自动复制),要么共享内存(把 JS 的 Uint8Array 直接传进去)。对于大数组,必须用共享内存零拷贝,否则复制开销会吃掉所有性能收益。上面的灰度例子就是零拷贝——imageData.data 的 buffer 直接被 WASM 当作自己的内存读写。

3. 调试和工程化复杂度。Rust 写的 WASM 出错时,stack trace 里是 WASM 地址,不是源码行号;虽然 wasm-pack 支持 source map 和 DWARF 调试信息,但调试体验远不如纯 JS。CI 流程要加上 Rust 工具链、交叉编译、产物缓存。团队还要有人懂 Rust/C++——这是隐性最大成本。

八、生态现状:WASM 已经是重度 Web 应用的基础设施

别以为 WASM 还停留在"玩具"阶段——下面这些主流产品早就把它当基础设施:

  • Figma:浏览器里跑 AutoCAD 级别的矢量编辑器,核心 C++ 引擎整个编译成 WASM,这是它流畅的根本原因。
  • Photoshop Web:Adobe 把 Photoshop 的核心图像管线(SSE 指令优化的 C++)搬到 WASM,跑在 Web 上。
  • Google Earth:整个 3D 渲染引擎是 C++ 编译的 WASM。
  • 1Password / Bitwarden:端到端加密的密码学计算走 WASM,既快又安全。
  • Discord / Zoom:屏幕共享的视频编解码用 WASM 降级方案,在浏览器原生编码不可用时代替。

WebAssembly 现在已经不止跑在浏览器——通过 WASI(WebAssembly System Interface),它能跑在服务端、边缘节点(Cloudflare Workers、Fastly Compute 都支持)、甚至嵌入式设备。一次编译,到处跑的承诺,正在被严肃对待。

但回到前端这个语境,我的判断是:WASM 不会出现在每个项目里,但只要你的项目遇到"JS 撑不住的计算瓶颈",WASM 几乎是唯一解。学会判断该不该用,比学会怎么用更重要。

小结

  • WASM 是什么:浏览器可执行的二进制字节码,C/Rust/Go 编译成 .wasm 跑,AOT 编译 + 静态类型,CPU 密集场景接近原生速度。
  • 和 JS 的关系:互补不是替代。JS 做 DOM/交互/网络,WASM 做纯计算,通过 wasm-bindgen 这类胶水桥接。
  • 怎么用:Rust(wasm-pack + wasm-bindgen)写逻辑 → 编译成 .wasm + JS 胶水 → JS import 调用,大数组走共享内存零拷贝。
  • 甜区:图像/视频处理、3D/游戏引擎、加密压缩、大型数据计算、复用 C/C++ 库(FFmpeg/SQLite/PDF)到 Web。
  • 反甜区:DOM 操作、简单逻辑、I/O 密集、高频跨边界——这些上了 WASM 反而更慢。

金句:WebAssembly 不是要取代 JavaScript,而是让 JavaScript 终于能干它原本干不了的活——CPU 密集这块拼图补上之后,浏览器才真正成了"什么应用都能跑"的平台。

Related

相关推荐