WebAssembly (Wasm) 是一种面向 web 的低级字节码格式,它被设计用于高效地在 web 浏览器中运行。相比于 JavaScript,WebAssembly 在很多情况下能够提供更好的性能,特别是 CPU 密集型计算。
结论
经实测发现,使用 WebAssembly 计算大文件的 MD5 哈希值相较于 JavaScript 实现,耗时缩减了一半,这种改进不仅可以显著提升用户体验,还能够加速网页加载速度,提高整体性能。这种技术优化可以为用户提供更快速、更流畅的体验,特别是在处理大型文件或数据时,效果更为明显。
1. 从 0 开始用 rust + wasm 实现 MD5 工具
1.1 安装 rust 工具链 cargo
具体参考:www.rust-lang.org/tools/insta…
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
1.2 安装 wasm-pack
具体参考:rustwasm.github.io/wasm-pack/i…
cargo install wasm-pack
1.3 安装 cargo-generate
cargo install cargo-generate
1.4 创建一个 rust+wasm 项目
// 创建项目
cargo new --lib md5-util
// 安装依赖
cargo add wasm-bindgen
cargo add hex
cargo add md-5
1.4.1 Cargo.toml
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2021"
[dependencies]
hex = "0.4.3"
md-5 = "0.10.6"
wasm-bindgen = "0.2.92"
[lib]
crate-type = ["cdylib", "rlib"]
1.4.2 lib.rs,这段代码就是给导出一个 js 可以饮用的胶水方法,calculate_md5,用于计算 md5,入参类型为 Unit8Array,返回值类型为 String
// src/lib.rs
use md5::{Md5, Digest};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn calculate_md5(input: &[u8]) -> String {
let mut hasher = Md5::new();
hasher.update(input);
let result = hasher.finalize();
hex::encode(result)
}
可以看一下生成的源码,具体就不赘述了。
1.4.3 编译 wasm
// 根目录执行
wasm-pack build --target web
这时候可以看到 pkg 里面就是编译结果,下面由一段 js 代码来示例如何使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculate File MD5 Hash</title>
</head>
<body>
<input type="file" id="fileInput">
<button id="calculateBtn">Calculate MD5</button>
<div id="result"></div>
<script type="module">
import init, { calculate_md5 } from './pkg/md5_util.js';
// SIZE 的单位为页,一页64KB
const SIZE = 10000;
// 初始化 wasm
init().then(instance => {
instance.memory.grow(SIZE);
});
const calculateMD5 = () => {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
console.time('读取文件耗时');
reader.onload = (e) => {
console.timeEnd('读取文件耗时');
console.time('获取 MD5 耗时');
const hash = calculate_md5(new Uint8Array(e.target.result));
console.timeEnd('获取 MD5 耗时');
document.getElementById('result').innerText = `MD5 hash of ${file.name}: 【${hash}】`;
};
} else {
document.getElementById('result').innerText = 'Please select a file.';
}
}
const calculateBtn = document.querySelector('#calculateBtn');
calculateBtn.addEventListener('click', calculateMD5);
</script>
</body>
</html>
1.4.4 在 WebWorker 中使用 wasm
这种大计算量的方法阻塞主线程是不友好的,我们可以让 wasm 运行在 Worker 里,不阻塞主线程。
主线程代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculate File MD5 Hash</title>
</head>
<body>
<input type="file" id="fileInput">
<button id="calculateBtn">Calculate MD5</button>
<div id="result"></div>
<script type="module">
const worker = new Worker('./test_worker.worker.js');
worker.onmessage = (message) => {
const { action, value } = message.data;
switch (action) {
case 'md5_result':
console.timeEnd('获取 MD5 耗时');
document.getElementById('result').innerText = `MD5: 【${value}】`;
break;
default:
break;
}
};
const calculateMD5 = () => {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
console.time('读取文件耗时');
reader.onload = (e) => {
console.timeEnd('读取文件耗时');
console.time('获取 MD5 耗时');
const fileData = new Uint8Array(e.target.result);
// 使用可转移对象,减少拷贝
worker.postMessage({
action: 'calculate_md5',
value: fileData,
}, [fileData.buffer]);
// 不使用可转移对象
// worker.postMessage({
// action: 'calculate_md5',
// value: fileData,
// });
};
} else {
document.getElementById('result').innerText = 'Please select a file.';
}
}
const calculateBtn = document.querySelector('#calculateBtn');
calculateBtn.addEventListener('click', calculateMD5);
</script>
</body>
</html>
Worker:
let isInit = false;
let calculate_md5 = () => {};
// SIZE 的单位为页,一页64KB
const SIZE = 10000;
import('./pkg/md5_util.js').then(async (wasmJsModule) => {
console.log('wasmJsModule: ', wasmJsModule);
const init = wasmJsModule.default;
calculate_md5 = wasmJsModule.calculate_md5;
const instance = await init();
instance.memory.grow(SIZE);
isInit = true;
});
self.addEventListener('message', message => {
const { action, value } = message.data;
switch (action) {
case 'calculate_md5': {
const result = calculate_md5(value);
console.log('result: ', result);
self.postMessage({
action: 'md5_result',
value: result,
});
break;
}
default:
break;
}
});
2. 性能对比
| 文件大小 | js 耗时 | wasm + rust 耗时 |
|---|---|---|
| 119MB | 1123ms | 460ms |