WebAssembly:前端性能优化的终极利器
WebAssembly是一种二进制代码格式,可将C/C++/Rust等语言编译为.wasm文件在浏览器中运行。本文介绍了使用AssemblyScript快速实现WebAssembly开发的方法:通过配置编译环境,将TypeScript代码编译为wasm模块,并详细说明了产物文件的作用。文章还提供了一个性能对比demo,展示WebAssembly在处理斐波那契数列等计算密集型任务时的性能优势。测试显
WebAssembly(wasm)是一种二进制代码格式,C、C++、Rust、Go编译成.wasm后缀名的文件在浏览器中运行。
这个技术可能很多前端没听说过,WebAssembly技术是在极端性能优化上用到的技术,可能很多前端在工作中很少遇到极端性能优化情况,大部分前端只知道虚拟表格,虚拟列表,安需加载,安需导入,缓存优化,Web Workers。
WebAssembly技术对于复杂的3D,WebGL,十亿行数据表格绘制场景的更优解决方案。
官方文档:WebAssembly
WebAssembly使用场景
- 复杂计算:WebAssembly 主要解决 JavaScript 在计算密集型任务中的性能瓶颈。通过将这些计算任务交给webAssembly 执行,可以大大提高处理效率。举个例子,某些科学计算、数据处理、机器学习任务等,可以通过 Rust或C++编写,然后编译成 WASM 进行浏览器端执行
- 图开会制:在图形渲染领域,WebAssembly能够加速基于 WebGL 或其他图形渲染库的任务。例如,使用 Rust编写高效的图形绘制库,如 skia,并将其编译为 WebAssembly,从而在浏览器中实现高性能的图形渲染。
- 音视频编辑:WebAssembly 适用于音视频编辑、处理和转换等高性能应用。与 JavaScript 相比,webAssembly 提供了更强的性能,能够实时处理音视频流,进行高质量的编辑和渲染。
- 高性能渲染库:使用 Rust 或类似语言编写的高性能渲染库可以通过 WebAssembly 在浏览器中运行,提供比JavaScript 更高效的渲染能力。例如,Rust 与 skia(一个高效的2D 图形渲染库)结合,能够在浏览器中提供极快的渲染性能,适用于图形密集型应用如游戏或 UI 动画。
简单入门分四步
- 编写代码
- 将代码编译为 wasm
- 在前端加载 wasm
- 调用wasm提供的方法
下面开始写demo
Typescript也能够去做wasm事情,如果不想自己写可以借助三方库 **assemblyscript**,用这个库可以用最简单最少量的成本接触WebAssembly.
步骤如下:
一,先在建一个文件夹 1.asc-demo(在node终端下执行:mkdir '1.asc-demo')

二,再cd到目录下(cd 1.asc-demo)
三,初始化工程(pnpm init)会生成一个package.json文件

四,安装assemblyscript(pnpm add assemblyscript -D)

五,使用assemblyscript编译,先在asc-demo目录下建个文件夹assembly (mkdir assembly)

再assembly目录下建一个文件index.js (touch assembly/index.ts)
然后再配置编译脚本,对应的编译文件就是刚刚建的 assembly/index.ts
"asbuild:release": "asc assembly/index.ts --target release"
接下来在根目录下还需要一个文件,asconfig.json
{
"targets":{
"release":{
"outFile":"build/release.wasm",
"textFile":"build/release.wat",
"sourceMap":true
}
},
"options":{
"bindings":"esm"
}
}
配置说明:
targets 配置
targets
定义了不同的编译目标配置:
- release:个编译目标的名称
- outFile:
"
build/release.wasm"
- 指定编译后的 WebAssembly 文件输出路径 - textFile:
"
build/release.wat"
- 指定生成的 WAT 文件路径(WebAssembly Text Format,是 WASM 的文本表示形式,方便阅读和调试) - sourceMap:
true
- 是否生成源码映射文件,用于调试时将 WASM 代码映射回原始的 AssemblyScript 代码
- outFile:
options 配置
options
定义全局编译选项:
- bindings:
"esm"
- 表示生成 ES Module 格式的 JavaScript 绑定文件,用于在 JavaScript 中更方便地调用 WASM 模块
这样基本配置就完成了。
六,配置完成后就可以在assembly/index.ts中编写业务代码了。
我们写几个测试代码,加/减法,斐波拉契数列
export function add(a: number, b: number): number {
let res: number = 0
for (let i = 0; i < 1000000000; i++) {
res = 0;
}
return a + b;
}
export function sub(a: number, b: number): number {
return a - b;
}
// 斐波那契数列 - 递归实现(性能测试用)
export function fib(n: number): number {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
// 斐波那契数列 - 动态规划实现
export function fibDP(n: number): number {
if (n < 0) {
return n;
}
if (n === 0 || n === 1) {
return n;
}
let dp: number[] = [];
dp[0] = 0;
dp[1] = 1;
let index = <i32>n; // 显式转换为 i32
for (let i = 2; i <= index; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[index];
}
因为是ts项目还需要建一个tsconfig.json文件
{
"extends": "../node_modules/assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}
七,执行 pnpm asbuild:release 编译成wasm文件
执行完pnpm asbuild:release后,会生成一个build目录,这个目录中的文件就是编后的二进制文件
现在产物就出来了,下面是产物各文件的介绍:
1. release.wasm (WebAssembly 二进制文件)
- 最重要的文件,是实际运行的 WebAssembly 模块
- 二进制格式,体积小,加载快
- 在浏览器或 Node.js 中实际执行的文件
- 使用示例:javascript
const wasmModule = await WebAssembly.instantiateStreaming( fetch('release.wasm') );
2. release.wat (WebAssembly 文本格式)
- WebAssembly 的人类可读文本表示
- 用于调试和学习 WebAssembly 的工作原理
- 可以看到函数、内存布局、指令等
- 示例内容:wat
(module (func $add (param $0 f64) (param $1 f64) (result f64) local.get $0 local.get $1 f64.add ) )
3. release.js (JavaScript 绑定文件
重要,链接wasm文件和前端工程的胶水层代码
- 除memory是自带的,add,sub,fib,fibDP这四个方法就是在assembly/index.ts中编写的实例方法;
- 通过下面的引用使用到前端工程中:javascript
import { add, sub, fib } from './build/release.js'; console.log(add(1, 2)); // 直接调用
4. release.d.ts (TypeScript 类型定义)
- TypeScript 类型声明文件
- 提供导出函数的类型信息
- 让 TypeScript 项目能获得类型提示
- 内容示例:TypeScript
export declare function add(a: number, b: number): number; export declare function sub(a: number, b: number): number; export declare function fib(n: number): number;
5. release.wasm.map (Source Map 文件)
- 源代码映射文件
- 用于调试:将 WASM 代码映射回原始 TypeScript 代码
- 在浏览器开发工具中可以看到原始代码而不是 WASM
- 生产环境通常不需要
开发时:所有文件都有用
.wasm
- 运行.js
- 方便调用.d.ts
- 类型提示.wat
- 调试和学习.wasm.map
- 调试
生产环境:通常只需要
.wasm
- 必需.js
- 如果使用 JS 包装器.d.ts
- 如果是 TypeScript 项目
最小部署:如果手动加载,只需要 .wasm
文
在web端为什么可以使用 WebAssembly呢?
因为浏览在控制台输入WebAssembly就有这个对像
注:如果遇见 控制台输入 WebAssembly 点不开,看不了对像详情,可以使用console.dir(WebAssembly)
浏览器原生就支持,在node服务器端同样也是有WebAssembly这个对像,有了这个对像,就可以通过 WebAssembly.instantiate 取到对应的方法
使用 fetch 请求获取 assembly/index.ts 编译后得到的 release.wasm 方法 memory,add,sub,fib,fibDP
八,在WEB端使用
在根目录下建一个文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import {add} from './build/release.js'
console.log(add(1,2))
</script>
</body>
</html>
再给一个完整的测试对比demo
根目录下的index.html代码在下面,运行index.html文件前需要先执行 pnpm asbuild:release


index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebAssembly vs JavaScript 性能测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-size: 14px;
}
h1 {
font-size: 24px;
}
h2 {
font-size: 18px;
margin-top: 20px;
}
.test-section {
border: 1px solid #ddd;
padding: 15px;
margin: 15px 0;
border-radius: 5px;
}
.controls {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
}
input[type="number"] {
width: 80px;
padding: 5px;
}
button {
padding: 5px 15px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 3px;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.result-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.result-table th, .result-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.result-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.faster {
color: green;
font-weight: bold;
}
.slower {
color: #666;
}
.warning {
color: orange;
font-size: 12px;
margin-left: 10px;
}
#loading {
color: blue;
font-style: italic;
margin: 10px 0;
}
.clear-btn {
background-color: #dc3545;
font-size: 12px;
padding: 3px 10px;
}
.clear-btn:hover {
background-color: #c82333;
}
#moduleStatus {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.loading {
background-color: #f0f0f0;
color: #666;
}
.ready {
background-color: #d4edda;
color: #155724;
}
.error {
background-color: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<h1>WebAssembly vs JavaScript 性能测试</h1>
<div id="moduleStatus" class="loading">正在加载 WebAssembly 模块...</div>
<div class="test-section">
<h2>递归版本测试</h2>
<div class="controls">
<label>输入 n 值:</label>
<input type="number" id="recursiveInput" min="1" max="45" placeholder="例如:30">
<button id="recursiveBtn" onclick="runRecursiveTest()" disabled>运行递归测试</button>
<span class="warning">注意:n > 40 会非常慢</span>
<button class="clear-btn" onclick="clearResults('recursiveResults')">清空结果</button>
</div>
<div id="recursiveLoading"></div>
<table class="result-table" id="recursiveResults" style="display:none;">
<thead>
<tr>
<th>n 值</th>
<th>结果</th>
<th>JavaScript 时间 (ms)</th>
<th>WebAssembly 时间 (ms)</th>
<th>性能提升</th>
<th>递归调用次数</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="test-section">
<h2>动态规划版本测试</h2>
<div class="controls">
<label>输入 n 值:</label>
<input type="number" id="dpInput" min="1" max="100000" placeholder="例如:1000">
<button id="dpBtn" onclick="runDPTest()" disabled>运行 DP 测试</button>
<span class="warning">可以测试很大的数值</span>
<button class="clear-btn" onclick="clearResults('dpResults')">清空结果</button>
</div>
<div id="dpLoading"></div>
<table class="result-table" id="dpResults" style="display:none;">
<thead>
<tr>
<th>n 值</th>
<th>结果</th>
<th>JavaScript 时间 (ms)</th>
<th>WebAssembly 时间 (ms)</th>
<th>性能提升</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script>
// 先定义全局函数占位符
window.runRecursiveTest = function() {
alert('WebAssembly 模块还在加载中,请稍候...');
}
window.runDPTest = function() {
alert('WebAssembly 模块还在加载中,请稍候...');
}
window.clearResults = function(tableId) {
const table = document.getElementById(tableId);
const tbody = table.querySelector('tbody');
tbody.innerHTML = '';
table.style.display = 'none';
}
</script>
<script type="module">
const moduleStatus = document.getElementById('moduleStatus');
try {
// 导入 WebAssembly 模块
const { fib: wasmFib, fibDP: wasmFibDP } = await import('./build/release.js');
// JavaScript 递归版本
function jsFib(n) {
if (n <= 1) {
return n;
}
return jsFib(n - 1) + jsFib(n - 2);
}
// JavaScript DP 版本
function jsFibDP(n) {
if (n <= 1) return n;
let dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// 计算递归调用次数
function countRecursiveCalls(n) {
if (n <= 1) return 1;
return countRecursiveCalls(n - 1) + countRecursiveCalls(n - 2) + 1;
}
// 定义真正的测试函数
window.runRecursiveTest = async function() {
const input = document.getElementById('recursiveInput');
const n = parseInt(input.value);
if (!n || n < 1) {
alert('请输入有效的 n 值(大于 0)');
return;
}
if (n > 40) {
if (!confirm(`计算 fib(${n}) 可能需要很长时间,确定继续吗?`)) {
return;
}
}
const loadingDiv = document.getElementById('recursiveLoading');
const table = document.getElementById('recursiveResults');
const tbody = table.querySelector('tbody');
loadingDiv.textContent = `正在计算递归 fib(${n})...`;
await new Promise(resolve => setTimeout(resolve, 10));
try {
// 测试 JavaScript 版本
const jsStart = performance.now();
const jsResult = jsFib(n);
const jsEnd = performance.now();
const jsTime = jsEnd - jsStart;
// 测试 WebAssembly 版本
const wasmStart = performance.now();
const wasmResult = wasmFib(n);
const wasmEnd = performance.now();
const wasmTime = wasmEnd - wasmStart;
// 计算性能差异
const speedup = jsTime / wasmTime;
// 计算递归调用次数(只对小数值计算)
const calls = n <= 25 ? countRecursiveCalls(n) : '太大无法计算';
// 添加结果到表格
const row = tbody.insertRow(0);
row.innerHTML = `
<td>${n}</td>
<td>${wasmResult}</td>
<td class="${speedup > 1 ? 'slower' : 'faster'}">${jsTime.toFixed(3)}</td>
<td class="${speedup > 1 ? 'faster' : 'slower'}">${wasmTime.toFixed(3)}</td>
<td><span class="faster">${speedup.toFixed(2)}x</span></td>
<td>${calls}</td>
`;
table.style.display = 'table';
loadingDiv.textContent = '';
} catch (error) {
loadingDiv.textContent = `错误: ${error.message}`;
console.error(error);
}
}
window.runDPTest = async function() {
const input = document.getElementById('dpInput');
const n = parseInt(input.value);
if (!n || n < 1) {
alert('请输入有效的 n 值(大于 0)');
return;
}
const loadingDiv = document.getElementById('dpLoading');
const table = document.getElementById('dpResults');
const tbody = table.querySelector('tbody');
loadingDiv.textContent = `正在计算 DP fib(${n})...`;
await new Promise(resolve => setTimeout(resolve, 10));
try {
// 测试 JavaScript 版本
const jsStart = performance.now();
const jsResult = jsFibDP(n);
const jsEnd = performance.now();
const jsTime = jsEnd - jsStart;
// 测试 WebAssembly 版本
const wasmStart = performance.now();
const wasmResult = wasmFibDP(n);
const wasmEnd = performance.now();
const wasmTime = wasmEnd - wasmStart;
// 计算性能差异
const speedup = jsTime / wasmTime;
// 添加结果到表格
const row = tbody.insertRow(0);
row.innerHTML = `
<td>${n}</td>
<td>${n < 30 ? wasmResult : '数值过大'}</td>
<td class="${speedup > 1 ? 'slower' : 'faster'}">${jsTime.toFixed(3)}</td>
<td class="${speedup > 1 ? 'faster' : 'slower'}">${wasmTime.toFixed(3)}</td>
<td><span class="${speedup > 1 ? 'faster' : ''}">${speedup.toFixed(2)}x</span></td>
`;
table.style.display = 'table';
loadingDiv.textContent = '';
} catch (error) {
loadingDiv.textContent = `错误: ${error.message}`;
console.error(error);
}
}
// 绑定回车键
document.getElementById('recursiveInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') runRecursiveTest();
});
document.getElementById('dpInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') runDPTest();
});
// 启用按钮
document.getElementById('recursiveBtn').disabled = false;
document.getElementById('dpBtn').disabled = false;
// 更新状态
moduleStatus.className = 'ready';
moduleStatus.textContent = '✓ WebAssembly 模块加载成功,可以开始测试';
console.log('WebAssembly 模块已加载,可以开始测试');
} catch (error) {
console.error('加载 WebAssembly 模块失败:', error);
moduleStatus.className = 'error';
moduleStatus.innerHTML = `✗ WebAssembly 模块加载失败: ${error.message}<br>
请确保已运行 <code>pnpm asbuild:release</code> 并且使用 HTTP 服务器访问页面`;
}
</script>
</body>
</html>
代码文档在github上:
https://github.com/xufangfang-99/WebAssemblyDemo
更多推荐
所有评论(0)