使用 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()
}

相关

相关推荐
平头哥~1 小时前
Day 07 _ 那条 1px 的线,为什么在手机上忽粗忽细
前端·css·样式·学习资料
小江的记录本1 小时前
【CSS】CSS 动画:transition、animation、transform、CSS3 新特性(附《思维导图》)
前端·css·安全·spring·前端框架·css3·动画
circuitsosk1 小时前
任务规划器的三种范式对比:ReAct、Plan-and-Execute 与 Tree-of-Thought 在真实业务中的取舍
前端·javascript·python·react.js·llm·ai agent
hzxpaipai2 小时前
企业官网技术架构拆解:前端、后台、数据库、服务器如何协同
前端·数据库·架构
深念Y4 小时前
Lenovo XiaoXinAir 14 — 性能模式切换
前端·网络
计算机魔术师5 小时前
Meta突然杀进第一梯队:一个新模型,把AI推理成本打下来8倍
前端
IT_陈寒5 小时前
Redis缓存雪崩把我坑惨了,这次长记性了
前端·人工智能·后端
风骏时光牛马5 小时前
提示词工程:大模型效能挖掘与指令设计实战
前端
仓三5 小时前
Chrome DevTools MCP 上手:让 Coding Agent 自己调试前端页面
前端·chrome devtools·mcp