使用 wasm 提高前端20倍的 md5 计算速度

背景

  • 前阵子做大文件上传,本地内网测试发现非常慢,查了很久问题发现前端 MD5 计算消耗了非常多的时间。
  • 知道问题在哪那么就该想办法优化了,但以常规的优化很难有质的提升。
  • 最近不是流行 WebAssembly 吗,听说性能非常高,于是就决定自己写个 wasmMD5 库。
  • 看看 wasm 对前端能提升多少,也顺便体验下 WebAssembly 的开发。

开始

  • 此前我并未使用过 wasm 相关的东西,根据 mdn 的介绍可以用 c/c++rust 编写。
  • 我选择使用 rust 开发,并根据 mdn 的文档下载了 wasm-bindgen wasm 开发工具 (需要会使用 rust 相关工具链)
  • 折腾了两天编译了第一版 wasm 代码
  • 它是通过异步请求 .wasm 文件的形式来加载 wasm 程序的,这也是 mdn 上的标准做法
html 复制代码
<script type="module">
    import init, {Md5, Sha256, Sha512} from "./dist/digest_wasm.js"
    window.onload = async () => {
        await init() // 需要先初始化加载 wasm
        /* 后续的操作 */
    }
</script>
  • 这种是适用于 webnode 使用可能会有问题,找不到 wasm 文件或者用构建工具时没能正确将 wasm 一起打包
  • 研究了一下 wasm-bindgen 编译后的文件,发现可以删除掉一部分初始化代码改为 base64 内嵌代码,这样就不需要异步请求,同构为 web node 通用的代码
js 复制代码
// patch.mjs
import {readFile, writeFile, unlink} 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 方法来异步加载了 开箱即用

测试

  • 以防万一编译后有差错,需要编写一份测试脚本
js 复制代码
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 测试脚本检查是否有错误
shell 复制代码
wasm-pack build --release --target web --out-dir dist/lib && node patch.mjs && node test.mjs

调优

  • 默认编译配置是减少体积的,MD5 计算需要消耗大量时间,这种时候应该以性能为主
  • 修改 Cargo.toml 配置
toml 复制代码
[profile.release]
opt-level = 3 # 开启全面优化
lto = "z" # 开启编译时间更久更激进的优化
strip = true
panic = "abort"

[package.metadata.wasm-pack.profile.release]
wasm-opt = ["-O4"] # 这个应该是 LLVM 的参数 O4 优化

测速

  • 我的 CPU 是 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
  • 测试结果为 4G ZIP 文件计算 MD5 耗时 7-8 秒
  • SparkMd5的测速
  • 不知道是不是我使用的问题 SparkMd5 跑起来贼慢,而且跑起来笔记本风扇贼响
js 复制代码
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()
}

使用

  • pnpm install digest-wasm
js 复制代码
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()
    // const hash = Sha256.new()
    // const hash = Sha512.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()
}

相关

相关推荐
__zRainy__几秒前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.38 分钟前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~2 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华2 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子5 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅5 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni5 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学6 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
赵庆明老师7 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim