WebAssembly(wasm)是一种二进制代码格式,C、C++、Rust、Go编译成.wasm后缀名的文件在浏览器中运行。
这个技术可能很多前端没听说过,WebAssembly技术是在极端性能优化上用到的技术,可能很多前端在工作中很少遇到极端性能优化情况,大部分前端只知道虚拟表格,虚拟列表,安需加载,安需导入,缓存优化,Web Workers。
WebAssembly技术对于复杂的3D,WebGL,十亿行数据表格绘制场景的更优解决方案。
官方文档:WebAssembly


WebAssembly使用场景
  1. 复杂计算:WebAssembly 主要解决 JavaScript 在计算密集型任务中的性能瓶颈。通过将这些计算任务交给webAssembly 执行,可以大大提高处理效率。举个例子,某些科学计算、数据处理、机器学习任务等,可以通过 Rust或C++编写,然后编译成 WASM 进行浏览器端执行
  2. 图开会制:在图形渲染领域,WebAssembly能够加速基于 WebGL 或其他图形渲染库的任务。例如,使用 Rust编写高效的图形绘制库,如 skia,并将其编译为 WebAssembly,从而在浏览器中实现高性能的图形渲染。
  3. 音视频编辑:WebAssembly 适用于音视频编辑、处理和转换等高性能应用。与 JavaScript 相比,webAssembly 提供了更强的性能,能够实时处理音视频流,进行高质量的编辑和渲染。
  4. 高性能渲染库:使用 Rust 或类似语言编写的高性能渲染库可以通过 WebAssembly 在浏览器中运行,提供比JavaScript 更高效的渲染能力。例如,Rust 与 skia(一个高效的2D 图形渲染库)结合,能够在浏览器中提供极快的渲染性能,适用于图形密集型应用如游戏或 UI 动画。

简单入门分四步

  1. 编写代码
  2. 将代码编译为 wasm
  3. 在前端加载 wasm
  4. 调用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 代码

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

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐