背景
- 前阵子做大文件上传,本地内网测试发现非常慢,查了很久问题发现前端 MD5 计算消耗了非常多的时间。
- 知道问题在哪那么就该想办法优化了,但以常规的优化很难有质的提升。
- 最近不是流行 WebAssembly 吗,听说性能非常高,于是就决定自己写个
wasm
的 MD5 库。
- 看看
wasm
对前端能提升多少,也顺便体验下 WebAssembly 的开发。
开始
- 此前我并未使用过
wasm
相关的东西,根据 mdn 的介绍可以用 c/c++
或 rust
编写。
- 我选择使用
rust
开发,并根据 mdn 的文档下载了 wasm-pack wasm
开发工具 (需要会使用 rust
相关工具链)
- 折腾了两天编译了第一版
wasm
代码
- 它是通过异步请求
.wasm
文件的形式来加载 wasm
程序的,这也是 mdn 上的标准做法
<script type="module">
import init, {Md5, Sha256, Sha512} from "./dist/digest_wasm.js"
window.onload = async () => {
await init()
}
</script>
- 这种是适用于
web
的 node
使用可能会有问题,找不到 wasm
文件或者用构建工具时没能正确将 wasm
一起打包
- 研究了一下 wasm-pack 编译后的文件,发现可以删除掉一部分初始化代码改为
base64
内嵌代码,这样就不需要异步请求,同构为 web
node
通用的代码
import {readFile, writeFile} from "fs/promises"
const name = "digest_wasm"
const base = "./dist/lib"
const input = await readFile(`${base}/${name}.js`, "utf8")
const code = input
.replace(/async function __wbg_load[\s\S]*?(?=function __wbg_get_imports)/, "")
.replace(/async function __wbg_init[\s\S]*/, "")
.replace(/ *__wbg_init.__wbindgen_wasm_module = module;[\n\r]*/, "")
const output = `${code}
const __toBinary = (() => {
const table = new Uint8Array(128);
for (let i = 0; i < 64; i++) {
table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;
}
return (base64) => {
const n = base64.length
const bytes = new Uint8Array((n - (base64[n - 1] === "=") - (base64[n - 2] === "=")) * 3 / 4 | 0);
let i = 0, j = 0;
while (i < n) {
const c0 = table[base64.charCodeAt(i++)], c1 = table[base64.charCodeAt(i++)];
const c2 = table[base64.charCodeAt(i++)], c3 = table[base64.charCodeAt(i++)];
bytes[j++] = c0 << 2 | c1 >> 4;
bytes[j++] = c1 << 4 | c2 >> 2;
bytes[j++] = c2 << 6 | c3;
}
return bytes;
};
})();
const bytes = __toBinary(${JSON.stringify(await readFile(`${base}/${name}_bg.wasm`, 'base64'))});
initSync(bytes)
`
await writeFile(`${base}/../${name}.js`, output)
- 再次编译并使用
patch.mjs
来同构编译后的代码,这次不再有 init
方法来异步加载了 开箱即用
测试
import {Md5, Sha256, Sha512} from "./dist/digest_wasm.js"
import {readFile} from "fs/promises"
const data = await readFile("./LICENSE")
const md5 = await Md5.digest_u8(new Uint8Array(data))
console.assert(md5 === "d229da563da18fe5d58cd95a6467d584", "MD5 计算结果不正确")
const sha256 = await Sha256.digest_u8(new Uint8Array(data))
console.assert(sha256 === "1eb85fc97224598dad1852b5d6483bbcf0aa8608790dcc657a5a2a761ae9c8c6", "SHA256 计算结果不正确")
const sha512 = await Sha512.digest_u8(new Uint8Array(data))
console.assert(sha512 === "e2f81cb44129e1bc58941e7b3db1ffba40357889bace4fd65fd254d0be1bb757625bdf36bf46d555eb3ca4b130dcd1c05225caec28d8472dccf52a63dbd6e185", "SHA512 计算结果不正确")
构建
- 使用
wasm-pack --release --target web
打包成 js
代码
- 运行
patch.mjs
脚本修改为同构代码
- 运行
test.mjs
测试脚本检查是否有错误
wasm-pack build --release --target web --out-dir dist/lib && node patch.mjs && node test.mjs
调优
- 默认编译配置是减少体积的,
MD5
计算需要消耗大量时间,这种时候应该以性能为主
- 修改 Cargo.toml 配置
[profile.release]
opt-level = 3
lto = true
strip = true
panic = "abort"
[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-O4"]
测速
- 我的 CPU 是 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
- 测试结果为 4G ZIP 文件计算
MD5
耗时 7-8 秒
- 不知道是不是我使用的问题
SparkMd5
跑起来贼慢,而且跑起来笔记本风扇贼响
const fileHash = async (file, chunkSize = 128 << 20) => {
const hash = new SparkMD5.ArrayBuffer()
for (let i = 0; i < Math.ceil(file.size / chunkSize); i++) {
hash.append(await file.slice(chunkSize * i, chunkSize * (i + 1)).arrayBuffer())
}
return hash.end()
}
使用
import {Md5, Sha256, Sha512} from "digest-wasm";
const md5 = await Md5.digest("Hello World!")
const buffer = await new Blob(["Hello World!"]).arrayBuffer()
const sha256 = await Sha256.digest_u8(new Uint8Array(buffer))
const file_hash = async (file, chunkSize = 128 << 20) => {
const hash = Md5.new()
for (let i = 0; i < Math.ceil(file.size / chunkSize); i++) {
const chuck = await file.slice(chunkSize * i, chunkSize * (i + 1)).arrayBuffer()
await hash.update(new Uint8Array(chuck))
}
return hash.finalize()
}
相关