无阻塞、高性能:将Rust的imagequant库带入Web前端的奇妙之旅
我们先看下使用效果吧:exportx.dev
图片压缩一直是前端/客户端必须优化的问题之一,高质量低尺寸的文件能够提高响应速度,减少流量,目前不同规模的团队在不同场景可能有以下方式来解决这个问题:
- 服务端压缩:比如云对象存储 Cloudflare Images
- 代码自动压缩:比如前端脚手架压缩
- 手动压缩:比如Squoosh,TinyPng,当然还有exportx.dev
因为压缩目前还是最主流的压缩方式,并且Squoosh png压缩效果不佳(包太旧了),于是想自己重新再编译一套最新的wasm。
术语
squoosh:Chrome团队一个开源的客户端图片压缩网站
imagequant:一个处理png图像质量的库,可以减少图片的质量,本文用的是rust版本
wasm-pack:将rust打包成npm包的脚手架工具
crates:一个rust lib下载平台,类似于npm
WebAssembly简单入门
如果对WebAssembly的基础知识还不了解,可以先参考下MDN的文档。
https://developer.mozilla.org/zh-CN/docs/WebAssembly/Rust_to_wasm
WebAssembly的两种形态
首先简单介绍一下WebAssembly两种形态:
- 机器码格式
- 文本格式
这种文本形式更类似于处理器的汇编指令,因为WebAssembly本身是一门语言,一个小小的实例:
一般很少有人直接写文本格式,而是通过其他语言、或者是现存lib来编译成浏览器可用的wasm,这样很多客户端的计算模块只需简单处理都能很快转译成WASM在浏览器使用的模块,极大丰富了浏览器的使用场景。
接着我们先从一个入门实例开始,逐步到自己动手编译一个Rust模块。
编译 Rust 为 WebAssembly
编译
这里用官方文档文档种的 lib.rs 举例,从rust中可以看到:
- wasm是可以从js中获取函数调用,比如调用alert,或者拿到浏览器的DOM对象,比如大名鼎鼎的 https://yew.rs/ 框架是可以用Rust来开发网页。
- wasm也可以将自己的函数暴露给js
所以上面的代码属于套娃调用。
接下来就要将代码编译成js package,从Rust编译到wasm有比较成熟的脚手架工具使用:wasm-pack,文档参考
胶水代码
下面是从生成的js package里面截取一部分核心代码:
wasm-pack生成的胶水代码主要帮我们做了以下几件事:
- 加载和实例化WebAssembly模块(生命周期):处理模块的导入和导出,使它们能在JavaScript中使用。
- 类型转换:处理JavaScript和WebAssembly之间的类型转换,例如将复杂类型(如字符串、数组)进行编码/解码。
- 错误处理:将WebAssembly的错误转换为JavaScript可以识别的错误。
- 生成JavaScript API:为Rust WebAssembly模块创建一个JavaScript API,方便在JavaScript中调用。
- 生成npm包:创建一个可以发布到npm的包,方便其他JavaScript项目使用。
总的来说,胶水代码就是使WebAssembly模块能在JavaScript环境中更容易使用的一种代码。
浏览器使用
在浏览器种使用,wasm的加载和调用和js一样,因为通过d.ts描述wasm胶水层的代码,在开发上基本和平常一样。
兼容性
2024年了,应该不会还有人去兼容IE吧?
imagequant打包成npm包
压缩库选型
简单聊一下为什么选择imagequant,这也是调研得出来的结论,squoosh是开发者使用最多的一个的一个开源图片图片的网站(3年未更新),因此对于其他格式的压缩,可以部分copy其中一些比较优异的压缩库,不满意的部分比如png的可以自己编译wasm。
图片类型 | 压缩库 | 结论 |
---|---|---|
PNG | oxiPNG | squoosh使用的png压缩库,压缩率很一般,15-25%左右 |
PNG | imagequant | https://crates.io/crates/imagequant 压缩效果≈70% squoosh编译出来的wasm太老了(v2.12.1), 需要自己再编译一次,最新的是(v4.3.0) |
JPEG | mozJPEG | https://github.com/mozilla/mozjpeg 压缩效果≈80% |
WEBP | libwebp | https://github.com/webmproject/libwebp 压缩效果>90% |
SVG | libsvgo | https://github.com/svg/svgo 压缩效果10%~30% 原库svgo只支持node环境,libsvgo提供了浏览器的支持模式 |
AVIF | avif-serialize | https://github.com/packurl/wasm_avif 压缩效果>90%,但当前的兼容性差 squoosh使用的也比较旧, 且Figma不支持SharedArrayBuffer 重新编译了最新的avif-serialize |
压缩效果是如何鉴别的?
纯肉眼拖动观察肯定不够客观全面,所以我用了多张色彩对鲜明、和业务相关图片进行测验。
图片对比工具: https://www.diffchecker.com/image-compare/
PNG压缩打包
前面说到,sqoosh的oxipng压缩效果差、imagequant版本老,因此这里需要自己手动来打包
首先需要找到imagequant的rust库(crates类似npm)
https://crates.io/crates/imagequant
然后将依赖加入到Cargo.toml (这个类似package.json)
然后编写部分导出代码,将处理图片的函数暴露给js调用
打包生成npm package
可以先从d.ts中看生成的代码如何调用,从文件中看到需要输入uint8Array和图片尺寸大小,于是我们可以这样调用:
演示一下,压缩效果还不错,对于质量,还可以调整相关的参数。目前Exportx.dev 的参数设置为
instance.set_quality(35, 88);
压缩效果可以媲美tinypng,但是尺寸会比tinypng略大一些。
其他压缩库打包
其他库squoosh比如webp、jpg、avif已经帮忙打包好了,svg有现成的npm库,因此较为简单。
使用Worker避免阻塞js主线程
在压缩大图的时候,发现浏览器有点卡,周围的按钮的动效都无法正常运行,点也点不动。这是因为我们如果直接调用wasm会直接阻塞js主线程,既然是计算密集型的工作,这个时候就只能拿出非常适合这种场景的特性了:Worker。
先实现一下woker中需要执行的代码,他完成了2件事情
- 在worker中执行压缩任务
- 监听主线程发送的文件,传输文件到主线程
然后我们需要在主线程通过postmessage发送压缩异步任务
这样,压缩任务不占用主线程,并且可以并行多个压缩任务。
总结
编译imagequant的过程比较坎坷,主要是rust的语言机制确实跟平常使用的语言不一样,需要学习的概念会多一些。不过获得的效果还是很不错的:
- 节省了服务器处理资源
- 节省了图片网络传输的时间
- 接入了WebWorker,可以并发执行任务且不阻塞
- 接入Service worker后可以做到离线使用
感谢大家的阅读~
小广告:
- 个人博客:www.abfree.com
- 超快的浏览器压缩工具:exportx.dev
附录
Rust 镜像配置
代码参考:
© 2024 ABFree Co. All rights reserved.