上一篇文章(云函数冷启动优化)发布后,有人问:函数快了有什么用,用户上传一张 8MB 的图,光上传就要 6 秒,快的那 2 秒根本看不出来。
问得对。这次把链路的另一半补上:图片在「上传 → 存储 → 处理 → 下发」这条路上,每一层都在花钱和花时间,而我一开始只优化了中间那一层。
这篇文章的结论先放这里:同样一张用户手机直出的照片,从 8.2MB 压到 296KB------用户上传从 6.1 秒到 1.3 秒,加载从 3.4 秒到 0.14 秒,月账单从 78.4 元降到 9.6 元。参数全部实测过,文末有完整的对比表和一份可以照抄的参数组合。
测试环境
| 项 | 值 |
|---|---|
| 客户端 | 微信小程序(基础库 3.x) |
| 存储 | 腾讯云 COS + CDN |
| 图片处理 | 数据万象 imageMogr2(URL 参数式处理) |
| 测试图 | 手机直出 JPEG,4000×3000,8.2MB,质量约 92 |
| 网络环境 | 4G 下行实测约 2.5MB/s,上行约 0.8MB/s |
| 测量方式 | wx.uploadFile / wx.downloadFile 打点,每组 5 次取中位数 |
一、先画链路,再谈优化
一张图从用户手里到其他用户眼前,实际走这条路径:
四个环节对应三类钱:存储、处理次数、下行流量。压缩的意义不只是「加载快」,而是三层账单一起降------其中下行流量通常占大头。
我一开始的链路是:原图直接传 COS,云函数下载原图、用 SDK 处理、再传回去。相当于 8.2MB 的图在整个链路上走了两遍半。
二、上行端:上传前先压一道
现象:用户反馈上传卡住 5-6 秒。8.2MB ÷ 0.8MB/s ≈ 10 秒,实测中位数 6.1 秒------上行是最慢的一段,而这一段发生在用户手机上,跟服务器无关。
改法 :上传前用 wx.compressImage 先压一道。很多人不知道这个 API 可以指定压缩质量:
两个实测细节,文档里没写:
compressedWidth是等比缩放,不是裁剪,不用担心构图变形。
- 压缩失败的兜底必须写 。低版本基础库不认识
compressedWidth会直接报错,这时候裸quality也能压,别让用户的图传不上去。
效果:8.2MB → 1.8MB(宽 1600、质量 70),上传 6.1 秒 → 1.4 秒。
有取舍:compressedWidth: 1600 意味着服务端拿不到 4000px 的原图。先想清楚你的业务要不要高清原图------我做的是头像/内容图场景,1600px 绰绰有余;如果是证件识别类,这一步不能压宽度,只压质量。
三、下行端:数据万象参数逐项实测
下行端不走云函数,直接在 CDN URL 上带处理参数,图片在边缘节点完成处理:
三个参数各自贡献多少,我逐项测的(同一张 8.2MB 原图,5 次取中位数):
3.1 thumbnail:贡献最大,但别压过 750
| 参数 | 输出尺寸 | 体积 | 相比原图 |
|---|---|---|---|
| 无 | 4000×3000 | 8.2MB | --- |
| /thumbnail/1080x | 1080×810 | 1.86MB | -77% |
| /thumbnail/750x | 750×563 | 1.02MB | -88% |
| /thumbnail/500x | 500×375 | 540KB | -93% |
看起来 500x 更香?注意 750 这个分界线:主流手机的 2 倍屏上,内容图的显示宽度大约是 375 逻辑像素 × 2 = 750 物理像素 。750 以下在部分机型上能看出锯齿,750 是「肉眼看不出损」的下限。我的选择:列表用 750x,详情页用 1080x。
3.2 quality:甜点位在 75-80
在 1080x 基础上叠加 quality:
| quality | 体积 | 肉眼观感 |
|---|---|---|
| 90 | 1.52MB | 无差别 |
| 80 | 1.08MB | 无差别 |
| 75 | 910KB | 细看天空渐变有轻微色带 |
| 60 | 620KB | 明显色带,皮肤发闷 |
甜点位是 80:比 90 小 29%,肉眼无法区分。75 以下开始出现 JPEG 色带,别贪。
3.3 format/webp:再砍四成,但有个小程序专属坑
| 组合 | 体积 |
|---|---|
| /thumbnail/1080x/quality/80 | 1.08MB |
| /thumbnail/1080x/quality/80/format/webp | 640KB |
| /thumbnail/750x/quality/80/format/webp | 412KB |
| /thumbnail/750x/quality/75/format/webp | 296KB |
webp 比 JPEG 再省 40% 左右。但这里埋着小程序最经典的坑:
<image> 组件默认不渲染 webp。 需要显式加 webp 属性,而且只有基础库 2.9.0 以上支持;iOS 上动态 webp 不支持,静态 webp 没问题:
降级策略(这段建议原样抄走):URL 保留两个版本,组件按基础库版本选择:
3.4 别忘了缓存:URL 参数是缓存 key 的一部分
CDN 按「完整 URL(含参数)」缓存。这意味着两个坑:
- 尺寸参数要枚举,不要动态拼 。如果你把用户的滚动位置、屏幕宽度拼进 URL,每种组合都是一次缓存 miss + 一次源站处理,账单会爆炸。我的做法是全站只允许 3 种尺寸:
750x、1080x、原图。
- 参数顺序要固定 。
/thumbnail/750x/quality/80和/quality/80/thumbnail/750x是两个不同的缓存 key。封装一个buildCdnUrl,全站只走这一个函数。
四、账单:三层一起算
月度账单对比(图片为主的中小流量小程序,量级供参考,以你自己的账单为准):
| 计费项 | 优化前 | 优化后 | 说明 |
|---|---|---|---|
| COS 存储 | 42.0 GB | 9.1 GB | 上行端压缩后,COS 里存的就是 1.8MB 的压缩图,不再存 8.2MB 原图 |
| CDN 下行流量 | 96 GB | 3.9 GB | 降最多的一行,页面图全部命中 750x/1080x 两个枚举尺寸 |
| 图片处理次数 | 3.1 万次(云函数处理) | 5.4 万次(边缘处理) | 次数反而多了,但缩放/裁剪/格式转换在基础图片处理免费额度内 |
| 云函数出流量 | 88 GB | 0.2 GB | 上一篇的成果:函数只返回签名 URL,不再中转图片 |
| 月合计 | ¥78.4 | ¥9.6 | 降了 88%,其中下行流量和函数出流量贡献了九成以上 |
给个直观感受我实测的比例:下行流量是优化前的 约 4% (因为页面几乎所有图都命中了 296KB-640KB 这两档),处理费用不升反降------基础图片处理(缩放、裁剪、格式转换)本身在免费额度内,而原来云函数中转产生的流量和调用费才是大头。
一个反直觉的点 :压缩让「处理次数」变多了(每个尺寸都会处理一次),但因为处理发生在边缘、且结果被 CDN 缓存,摊到每个用户身上反而更便宜。处理一次、缓存一万次,永远比每个用户都搬运一遍原图划算。
五、总账
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 用户上传耗时(8.2MB 原图) | 6.1s | 1.4s | -77% |
| 图片加载耗时(详情页) | 3.4s | 0.14s | -96% |
| 单图下发体积 | 8.2MB | 296KB-640KB | -96% |
| CDN 下行流量 | 100% | 约 4% | -96% |
| 云函数图片中转 | 有 | 无 | 架构级移除 |
压缩分两头做,缺一不可:
- 上行 :
wx.compressImage(宽 1600 + 质量 70),省的是用户的时间和耐心
- 下行 :
imageMogr2(750x/1080x + quality 80 + webp),省的是账单和所有用户的加载时间
六、什么时候不要压这么狠
三条红线,都是想清楚再压:
- 业务需要原图的,上行端别压宽度 。证件识别、打印场景,
compressedWidth一压信息就没了,只压质量。
- 用户会二次编辑的,原图必须留。裁剪、打标这类功能需要原图做底,缩略图只是展示层。
- 别动态拼尺寸。全站枚举 2-3 个尺寸,参数顺序固定,否则 CDN 缓存形同虚设,处理费用会悄悄涨回来。
最后
一个被忽略的事实:这条链路上最贵的东西从来不是「处理图片」,而是「搬运图片」。把搬运砍掉,速度和账单一起好看了。
下一篇写这条链路剩下的最后一块骨头:云函数调数据万象时被一个权限配置卡了 3 天的排查记录------报错信息只说失败,不告诉你为什么。可以先关注,省得下次找不到。
自查清单(照着过一遍):
- 上传前压了吗?
wx.compressImage的降级分支写了吗?
- 下行 URL 带处理参数了吗?还是把原图直接甩给用户?
- 尺寸枚举了吗?有没有动态拼参数导致缓存 miss?
- webp 加属性了吗?低版本基础库的降级路径测过吗?
- 账单里「下行流量」那一行,这个月比上个月涨了多少?