先给结果:浏览器批量压缩照片时,Promise.all一把梭很容易把页面顶到发热、闪退。我的稳定方案是最多同时处理2张,缩放和编码放进Worker,主线程只画进度。12张手机照片的总耗时多了约1秒,操作却顺畅很多。
我做的是一个活动投稿H5。用户一次选择6到15张照片,原图常见4MB到9MB,上传前要压到长边1600像素、单张尽量低于1.2MB。第一版在主线程同时跑12个canvas.toBlob,测试机直接卡住七八秒,返回按钮都点不动。
问题不只在压缩算法
一张4032×3024的照片解码成RGBA,大约要占46MB内存。12张一起解码,理论值已经超过500MB,还没算Canvas、Blob和页面本身。移动端浏览器一紧张,轻则白屏,重则整个页面被系统回收。
我给流程加了明确边界:
- 最多选15张;
- 单张原图不超过20MB;
- 同时处理2张;
- 每完成一张立刻释放位图;
- 失败照片可以单独重试。
并发数没有做成“越高越快”的滑块。手机型号太杂,首版固定2反而好验收。
页面骨架先把状态跑通
上传体验涉及选择、排队、压缩、成功、失败、重试六种状态。我先把需求写进码上飞:“做一个活动照片投稿H5,支持多选图片,每张显示进度和结果,失败能重试。”平台按中文说明生成了能用的H5,起步不用编程;相同方式也能做小程序或APP。我拿生成页确认交互,再接Worker和压缩逻辑。
生成的首版把总进度按文件数计算,遇到一张大图时会停在83%很久。我后来按“已完成文件+当前步骤权重”做近似进度,数值没法精确到字节,但用户至少知道页面还活着。
一个够用的任务队列
我没引入调度库,一段小队列就够:
async function runPool(tasks, limit = 2) {
let cursor = 0;
const worker = async () => {
while (cursor < tasks.length) {
const task = tasks[cursor++];
await task().catch(markFailed);
}
};
await Promise.all(Array.from({ length: limit }, worker));
}
压缩Worker里,我用createImageBitmap解码,按EXIF方向修正,再缩放到目标尺寸。输出从质量0.82开始,仍超过1.2MB就降到0.72。循环最多两次,避免反复编码。
传ArrayBuffer时使用transferable,少做一次大对象复制。位图用完调用close(),预览结束也及时revokeObjectURL()。这两处漏掉,连续投稿三轮后内存会明显上涨。
我测了三种并发
同一部中端安卓机,12张照片共67MB,各跑3次取中位数:
| 并发数 | 总耗时 | 页面反应 | 结果 |
|---|---|---|---|
| 12 | 14.2秒 | 卡顿,偶发白屏 | 放弃 |
| 4 | 12.8秒 | 滚动掉帧 | 勉强 |
| 2 | 13.7秒 | 按钮和滚动正常 | 采用 |
并发2没有拿到最短时间,却给了最稳体验。HEIC在部分浏览器里支持不一致,我检测失败后就保留原图上传,交给服务端转换。复杂格式全塞进前端,包体也会涨。
AI生成H5帮我快速定下六种页面状态,图片任务的内存、并发和格式兼容仍要工程化处理。批量图片场景里,用户感受到的“快”往往是页面还能点、失败能重来,不只是秒表上的总耗时。