3.2 图片处理:CI 缩略图与水印

场景:列表页加载 30 秒的焦虑

网盘刚上线时,图片列表直接显示原图。用户打开一页 20 张设计稿,每张 5MB,页面要加载 100MB 才能渲染——慢到产品同学以为电脑坏了。更糟的是流量:手机用户走在路上打开列表,话费眼看消失。这个功能再不做,用户的耐心就耗光了。

做法有两个方向:客户端压缩,或云端生成缩略图。客户端压缩要求每张图上传时处理一次,费电费时而且换设备要重来。云端做一次、处处可用,是正解。云端这条路我们交给了数据万象 CI——它的缩略图是实时参数化生成的,不用预先存一份,参数即图片,存都不用存。

方案:缩略图的三条路

选型时比较过三种缩略图方案,差异主要在预生成还是实时生成:

方案 首次延迟 存储成本 参数调整灵活性 我们的选择
上传时预生成 无 每尺寸一份存储 差,尺寸固化 旧方案
客户端按需生成 低 无存储 好 淘汰
CI 实时处理 首次约 200 毫秒 无额外存储 极好 采用

CI 的实时处理还有个隐藏优点:改尺寸只改 URL 参数,老数据自动适用,不用批量重新生成。配合 CDN 缓存首次处理结果后,延迟也几乎消失。

CI 的图片处理还能把缩略图与水印合在一个 URL 里完成。水印参数的选择:

参数 作用 我们的取值
thumbnail 等比缩放 列表 200,详情 1080
watermark 贴水印图 右下角,半透明
format 转格式 webp 省流量
quality 质量 85 肉眼无差

配置:URL 参数即图片

CI 的图片处理不需要写后端代码,在 COS 的对象 URL 后面拼参数即可,也可以用 Go 拼:

// 列表缩略图:等比缩到 200 宽,转 webp
thumb := fmt.Sprintf("%s?imageMogr2/thumbnail/200x/format/webp/quality/85",
    cosURL(key))

// 加水印:右下角贴 logo,不透明度 70%
mark := fmt.Sprintf("%s?watermark/1/image/%s/gravity/southeast/opacity/70",
    cosURL(key), base64(logoURL))

水印图本身放在公开桶里(或 data 路径),文件名做 base64 编码后拼进参数。注意水印图建议用有透明通道的 png,否则贴出来是块色块,很难看。

CI 处理本身要开通服务并产生费用。我们的做法是只对用户上传目录开处理能力,默认关掉计费伴随的其余选项。开通入口在桶配置里,几分钟完事,文档在 cloud.tencent.com/document/product/860。

踩坑:四个图片处理的坑

第一个坑,水印防不了截屏党。水印只是提高盗图成本,挡不住裁剪与ps。要强保护加数字盲水印,CI 也有,但成本与清晰度代价不小,我们最后选择了看得见的水印加一句免责声明,务实为主。

第二个坑,webp 兼容性。webp 省流量是真的,但旧版浏览器与部分社交软件内置浏览器不认。我们的做法是按 User-Agent 决定格式,现代环境给 webp,其余给 jpg,两套 URL 由后端在渲染时决定。

第三个坑,实时处理被恶意刷。有人发现参数可拼,把同一张图生成上万个尺寸变体刷 CI 处理量。处理费是按调用计的,账单差点爆。防护是限制域名来源与尺寸白名单,只允许我们参数表里的几种尺寸,从根上断掉变体刷。

第四个坑,原图仍是公开风险。缩略图做了,水印贴了,但原图如果也在公开桶,一点就穿。所有带水印的预览链必须走签名与私有桶,原图的访问权限单独控制,水印是体面不是安全门锁。 关于成本,CI 图片处理是按次数收的,我们把账目拉出来看过:每月处理调用 400 万次,费用约 200 元。换来的是全站图片秒开与移动流量腰斩。这笔账极其划算,但要防刷:我们做了尺寸白名单与频率限制,不让恶意请求把处理量刷上十倍。

格式自适应的收益也有数据。把 jpg 全站切 webp 后,图片列表的平均流量降了 35%,肉眼画质无差。很多团队还没做只是因为懒,一个 User-Agent 判断就能省下三分之一流量。

优化项 上线前 上线后 变化
列表加载流量 100MB/页 2MB/页 降 98%
首屏渲染时间 8 秒 1 秒 降 87%
月度图片流量费 700 元 350 元 降 50%

再补一个水印设计上的小气思。水印图用谁家的 logo 折腾过两版:第一版用文字水印,被误会是图片自带标题;第二版换成半透明 logo 贴右下角,观感立刻正常。水印透明度 70% 是我们调出来的经验值,再低看不见,再高挡内容。设计同学说这个角度里有学问,我信了。

CI 还有个不大知名的能力:采样截图。视频上传后自动抽几帧做封面,列表里显示封面图比显示一个文件图标体验好太多。封面截图走的是同一套媒体任务流水线,几乎零新增代码。这种顺手能用的能力记得定期翻文档,CI 的功能清单比我以为的长。

顺带一句防盗链配合。图片处理后的 URL 直接暴露在页面里,第三方站点可以盗链。我们给预览域名加了 Referer 校验与签名,把白嫖流量挡在门外,细节在 4.3 防盗链一节。省钱和防刷是一对双胞胎,缺一个都守不住账单。 补一条 CI 盲水印的经验。盲水印肉眼看不出,能提取来源信息,适合溯源场景。我们给外部分享的图片加过一段盲水印,成本稍高,但对疑似盗用的情况能拿出证据。后来评估频率太低就下线了,作为策略保留在口袋里,遇到真有需要再开。成本与场景匹配,不是每个安全功能都值得常开。

动态图片也有注意点。gif 处理后的某些参数组合会破坏动画。我们的做法是动图默认只做格式转换与尺寸限制,不乱叠加水印参数,把可玩的功能留给静态图。isAnimated 类的判断在前端识别一下,成本几乎为零。

缓存层的设计要点再说细些。CI 的处理结果我们要让它命中 CDN,但万一文件内容变了却用着旧 URL 呢?解决方案是指纹参与 URL:参数里带文件版本哈希,内容变 URL 必变,缓存自然不会串。这套设计的收益是图片二次访问完全零延迟零成本。

CI 计费与 COS 计费的边界容易扯皮。开桶时如果同时开了图片处理的持续处理选项,部分操作两边都记账。我们出过一个月多收了几十块的小账,提工单问清楚后调整了配置。小账也是账,Config 一年下来够买台显示器。

最后给一个运营向建议:图片处理的参数我们做了管理后台可视化管理,而不是写死代码里。运营同学自己加个尺寸或改水印位置,发版都不用。这类灵活性属于给运营赋能,几十块工时换来长期不用改代码,划算。 再补一个存储省钱的细节。原图也可以做分层:一年前的项目素材直接转低频存储甚至归档,列表页只走缩略图。用户点详情才回源原图,这一下 mkdir 一样的小动作,每年存储费再降一档。分层逻辑细节在 5.2 生命周期里。

移动端还有一招:列表默认请求 webp 缩略图,Wi-Fi 环境再懒加载高清。用户流量宝贵,你对用户的流量慷慨,用户才会对你慷慨。 最后给一个排错指引:图片处理后显示破损,九成是参数顺序或编码问题。CI 的参数有先后语义,把 format 放在 thumbnail 前面可能会导致缩放前的原图被转码,多翻文档示例比瞎试快。参数报错时 CI 返回的信息很具体,别忽略。 写到这里发现还没提 base64 水印编码的坑。水印图的 base64 串里如果有加号或斜杠,不经处理直接拼 URL 会被解析错误,轻则水印不显示,重则整个图裂开。标准做法是 URL 安全编码后再拼,或者干脆对水印图单独走一个短链映射。这条坑官方文档里有,但我们依然花了半天踩过一遍。

性能数据也交代一下。加了 CDN 缓存后,缩略图的平均响应从首次的 200 毫秒降到二次访问的 30 毫秒内,p99 也稳定。用户对快的感知主要来自二次访问,所以第一次慢一点可以接受。前提是预览域名正确配置了缓存规则,这部分在 4.2 详述。

图片格式我们最终选了 webp 作为预览格式。同画质体积小三分之一,移动端流量友好。代价是老旧浏览器不兼容,我们做了格式协商,根据请求头降级到 jpeg,两全其美。

水印策略也有产品考量。公共素材加水印防盗用,私有文件不加水印免得用户烦。水印位置做过用户调研:左下角接受度最高,居中最为人厌。一个小参数的选择,背后是几十份问卷。

缩略图尺寸的设置也值得记录。最初只有一档两百像素,产品同学反馈列表页太小不够清晰。加了一档中等尺寸后,详情页打开时长反而降了,因为不用等原图加载完成。

小结

CI 把图片处理变成了 URL 参数拼图,零运维成本换来一个企业级图片管线。省下的开发时间我们投在了防盗刷尺寸白名单和格式自适应上,这两件事才是最容易翻车的地方。

延伸阅读