webAssembly
前言
前世今生
随着业务需求的复杂度不断提升,前端开发对性能的要求也越来越高,需要在保持流畅交互的同时支持更广泛繁杂的场景。比如在网页端运行 Unity 游戏引擎、运行 C/C++/C#/Rust 等代码等等,此时 webAssembly 应运而生,使得以多种语言编写的代码都可以接近原生的速度在 Web 中运行的途径,使得以前无法在 Web 上运行的客户端应用程序得以在 Web 上运行
js 性能瓶颈:js 是一种解释型、弱类型语言,没有静态变量类型,整个 js 代码在引擎中经历以下阶段:Parser —> AST —> Bytecode —> Machine Code —> Optimize Machine Code。但是由于没有静态变量类型,变量类型可能时刻在变,导致隐藏类变化,导致之前做的优化失去作用,重新优化,耗费时间
asm.js
webAssembly 的前身,一种更快的 js。是 js 语言的一个严格子集,通过
减少动态决议来帮助浏览器提升 js 优化空间
1 | function asmJs() { |
编译目标:一般来说开发人员不直接编写 asm.js 文件。对于 C++ 文件,一般使用 Emscripten 进行转换
asm.js 运行速度快于原生 js:虽然运行速度取决于不同的测试用例、硬件条件、浏览器引擎优化程度等等,不过一般来说其运行速度能达到原生 C/C++ 运行速度的 50% 甚至更高
- 减少动态决议:弱类型语言变成强类型语言。从而运行时不需要额外的类型推到
- 从而直接跳过了很多机制:自动 GC 机制、模板编程、JIT 优化等等,编译过程能完成更多事情,生成的机器码运行周期越短,代码运行地越快
asm 不足:asm 输出的还是 js 代码,即使优化的再好,也始终无法跳过 Parser 和 ByteCode Compiler,这两步在 js 代码执行过程中耗时较长
webAssembly
为解决 asm 的局限性,衍生出 webAssembly,它可以将 C/C++/.. 代码
绕过 js 直接生成机器码,一种可移植、体积小、加载快并且兼容 Web 的全新格式
.wat 文件为 .wasm 的文本格式,.wasm 为最终执行的二进制文件
- wasm 可理解为一种新的编程语言,和 html、css、js 并列为 web 领域的第四类编程语言
- wasm 为汇编语言,开发人员不直接编写,而由对应的编译器编译而来,二进制格式
- 主流浏览器可以直接读取并执行 wasm
编译过程
以 AssemblyScript 工具为例,展示 TS 源码编译成 wasm 的过程
AssemblyScript 是 TypeScript 的子集,映射到 WebAssembly 的低级类型体系:i32/i64/f32/f64、usize、静态数组、TypedArray;无 JS 对象/DOM。
编译器是 asc(AssemblyScript Compiler)。它用 TS 前端解析源码,做类型检查与优化,生成 WebAssembly IR,最终产出 .wasm(二进制)与可选 .wat(文本)
- 语法解析与类型检查:
- ts 代码解析成 AST
- 按 AssemblyScript 的受限类型系统(i32/i64/f32/f64/usize、静态数组、TypedArray 等)做类型检查与常量折叠;剔除 JS/DOM 不可达的部分
- 语义降级与中间表示:
- 泛型单态化:对每个具体类型实例生成专门版本(避免运行时装箱/反射)
- 类/对象布局确定:把类视为 线性内存中的记录结构” ,计算每个字段的定址偏移
- 闭包/箭头函数:捕获环境转为堆对象(环境块 + 目标函数引用),通过函数表或静态调用链接
- 控制流结构(if/for/while/try)映射为 Wasm 的结构化块和分支(block/loop/if/br_if)
- 低级指令选择与地址计算:
- 所有对象与数组都落在一块 线性内存 里,读写用
load<T>/store<T>,地址用指针 + 字节偏移。例如 f64 数组第 i 个元素地址:ptr + (i << 3)(8 字节对齐)
- 所有对象与数组都落在一块 线性内存 里,读写用
- 优化与代码生成:
- 使用 Binaryen/自带优化通道执行常量折叠、DCE、inlining、边界检查消除、循环简化等;不同
-O等级决定强度。 - 生成 Wasm 二进制(同时可选 .wat 以便审阅)
- 使用 Binaryen/自带优化通道执行常量折叠、DCE、inlining、边界检查消除、循环简化等;不同
文件结构
assembly/*.ts:仅供 Wasm 的 TS 源码(不要依赖项目的 Web/Node 代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78// 一份通用最小实现模板 assembly ts文件
// 与业务无关,只提供数值钳制和 Float64 列批处理两类基础能力,任何表格/数值场景都可复用
// 若要处理对象结构,保持“指针+长度”的列式传参,在 JS 侧序列化为 TypedArray 后再交给 Wasm
// 如需真正的批处理统计或结果结构,完善 batchProcess(当前是占位)和统计写回
export let processedRows: i32 = 0; // 已处理行数
export let totalTimeMs: f64 = 0; // 总耗时(ms)
// 钳制单个数值到区间 [min, max]。最基础的数值保护函数
export function optimizeNumber(value: f64, min: f64, max: f64): f64 {
if (value < min) return min;
if (value > max) return max;
return value;
}
// 对线性内存里从 ptr 开始、长度为 len 的 Float64Array 原地钳制。第 i 个元素地址为 ptr + (i<<3)
export function clampF64Array(
ptr: usize,
len: i32,
min: f64,
max: f64
): void {
for (let i = 0; i < len; i++) {
let off = ptr + ((<usize>i) << 3); // i * 8
let v = load<f64>(off);
if (v < min) v = min;
else if (v > max) v = max;
store<f64>(off, v);
}
}
// 先钳制再按精度四舍五入的批处理
// multiplier = 10^precision;multiplier <= 0 时跳过四舍五入
export function clampRoundF64Array(
ptr: usize,
len: i32,
min: f64,
max: f64,
multiplier: f64
): void {
const doRound: bool = multiplier > 0.0;
for (let i = 0; i < len; i++) {
let off = ptr + ((<usize>i) << 3);
let v = load<f64>(off);
if (v < min) v = min;
else if (v > max) v = max;
if (doRound) {
v = Math.round(v * multiplier) / multiplier;
}
store<f64>(off, v);
}
}
// 批处理占位实现,与 JS 包装器签名对齐
// 记录 processedRows = dataLen,返回占位指针 1024。耗时统计由 JS 侧负责
export function batchProcess(
dataLen: i32,
columnsLen: i32,
_cfgPtr: i32,
_cfgLen: i32
): i32 {
processedRows = dataLen;
totalTimeMs = 0.0; // timing handled in JS
// Return a placeholder pointer where a result structure could be written if needed
return 1024;
}
export function getProcessedRows(): i32 {
return processedRows;
}
export function getTotalTimeMs(): f64 {
return totalTimeMs;
}
export function cleanup(): void {
processedRows = 0;
totalTimeMs = 0.0;
}asconfig.json:多目标构建配置(debug/release)
- outFile/textFile/sourceMap:输出 .wasm、.wat、sourcemap
- optimizeLevel/shrinkLevel:优化与瘦身等级(release 用 O3)
- bindings: “esm”:生成 ESM 入口(便于现代前端工具)
- importMemory/exportMemory/initialMemory/maximumMemory:线性内存策略
- exportRuntime/exportTable/exportStart:是否导出运行时/表/start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
"targets": {
"release": {
"outFile": "public/wasm/table-optimizer.wasm",
"optimizeLevel": 3,
"shrinkLevel": 1,
"noAssert": true
},
"debug": {
"outFile": "public/wasm/table-optimizer-debug.wasm",
"debug": true
}
},
"options": {
"runtime": "stub",
"exportTable": false,
"exportRuntime": false
}
}产物与加载
- 产物:.wasm(运行)、可选 .wat(查看)、.map(调试)
- 前端加载(Vite/浏览器):把 .wasm 放静态目录(如 public/wasm/),作为静态资源 fetch
- Vite 建议:在开发服务器允许加载 wasm(如 assetsInclude: [‘*/.wasm’],必要时 COEP/COOP 头避免跨源隔离问题)
1
2
3
4
5
6
7
8const resp = await fetch("/wasm/table-optimizer.wasm");
const { instance } = await WebAssembly.instantiate(
await resp.arrayBuffer(),
{
js: { log: () => {}, yield: () => new Promise((r) => setTimeout(r, 0)) },
}
);
const { memory, clampRoundF64Array } = instance.exports;与 TypeScript 的差异/限制
- 仅支持与 Wasm 对齐的类型/标准库;不支持任意 JS/DOM/Node API
- 类/数组/字符串可用,但跨边界需 loader 或手动内存管理;性能敏感建议 TypedArray 指针传参
- 异常/垃圾回收与 JS 不同;长生命周期对象需注意 pin/unpin(若使用 loader API)
wasm 优势
性能优势
- 执行速度快:贴近硬件的指令集,并且执行前已经被编译成字节码,无需额外的解析、编译步骤
- 内存管理强:提供了更细粒度的内存管理能力,使用线性内存模型,所有的内存分配都是在一块连续的内存区域中进行,减少了 GC 的耗时
多语言支持
- 打破了 js 在 web 开发中的局限,支持多种编程语言,在 web 中充分发挥其他语言的优势
- 为跨平台共享代码提供了便利
安全性
- 沙盒环境:借助 web 开发的沙盒理念,有效避免了其他低级语言存在的额外攻击不好处理的问题
- 内存安全:特有的结构化的堆栈结构确保内存访问都是安全的,与其他一些低级语言不同,wasm 可以有效防止缓冲区溢出和其他常见的内存错误,避免了这些错误在传统编程中可能引发的安全漏洞
- 验证编译:预编译避免了在运行时进行潜在不安全的即时编译
应用场景
浏览器内应用
- 游戏开发:将 C、C++、Rust、Unity、Unreal Engine 等语言或引擎编写的游戏直接在 web 端运行,破除了以前只能在客户端运行的限制
- 多媒体处理:支持在 web 端处理视频编辑、3D 渲染、音频处理等需要大量计算的任务
跨平台应用
- 将原本只能在桌面端运行的应用程序,借助 wasm 轻松移植到 web 端
场景示例
1 | // 通用表格优化器 - WebAssembly 实现(静默版) |
1 | <script> |






