
给一个设备巡检系统做优化,用户上传的现场照片平均每张三兆多,列表页一屏九张,手机在车间信号差的地方打开要五六秒。查了流量构成,八成带宽花在图片上,而且列表页用的就是原图。后来搭了一条压缩流水线,上传后自动生成三个规格,列表用小图、详情用中图、导出用原图。改完流量降了一半多,列表页打开时间从五秒多降到一秒。这篇把流水线的设计和参数选择写清楚。
巡检记录列表,每条带一张照片缩略图。前端直接把原图地址塞进图片标签,浏览器把三兆的图下载下来再缩成八十像素显示,等于为了看一个指甲盖大小的图,传了一张能打印的照片。九张图并发下载,车间的无线网一挤,五秒白屏是常态,用户以为系统坏了。
拆开看三块。原图直出:没有规格区分,所有场景都下载原图。无缓存:图片地址带随机参数,每次都是新地址,浏览器缓存和边缘缓存全部失效。重复下载:同一张图在列表、详情、审批三个页面各下一遍,互不复用。三块叠加,一张图被下载三次、每次都走回源,流量自然下不来。
不同场景对清晰度的要求完全不同。列表缩略图八十到两百像素,压到几十千字节足够;详情页看细节,宽度一千像素左右、质量稍高;导出和存档要原图,不做有损压缩。按场景定规格,比笼统地说"压缩一下"有用得多,因为它明确了每张图该压到什么程度。
上传完成后不阻塞用户,写一条任务进队列,由后台任务消费。图片存进对象存储之后触发处理,处理完把多个规格写回存储,路径按"原图标识/规格名"组织。用户在列表页看到的永远是处理完成的图,没处理完的显示占位图。这条链路的关键是异步:压缩一次几十毫秒到几百毫秒,放在上传请求里同步做,用户体验会被拖慢。
规格写成一张表,处理函数读表执行,新增规格不用改代码:
const sharp = require('sharp');
const PROFILES = [
{ name: 'thumb', width: 200, quality: 72, format: 'webp' },
{ name: 'medium', width: 1000, quality: 80, format: 'webp' },
{ name: 'large', width: 1600, quality: 84, format: 'jpeg' }
];
async function buildVariants(inputPath, outputDir, id) {
const base = sharp(inputPath, { failOn: 'none' }).rotate(); // 按 EXIF 摆正
const meta = await base.metadata();
const tasks = PROFILES.map(async (p) => {
// 不放大:原图比目标规格小就保持原宽
const width = Math.min(p.width, meta.width || p.width);
const buf = await base
.clone()
.resize({ width, withoutEnlargement: true })
.toFormat(p.format, { quality: p.quality, mozjpeg: true })
.toBuffer();
return { name: p.name, bytes: buf.length, write: () => save(outputDir, id, p.name, buf) };
});
return Promise.all(tasks);
}几个参数说明。缩略图质量七十二是在对比过多档之后选的,再往上加文件明显变大,肉眼在手机上分辨不出。宽度用不放大策略,原图本身很小的时候不强行拉大,避免模糊。摆正这一步别省,手机拍的照片方向信息在元数据里,不处理会出现横着的图。
新一代格式在同质量下体积更小,但不是所有浏览器都支持。做法是同时生成一份通用格式和一份新格式,前端用图片标签的多个来源让浏览器自己挑,不支持的自动回退。转格式这一步由服务端统一做,不要求前端上传时就选,因为用户上传的都是手机原图,格式五花八门。
手机照片里带着拍摄地点、设备型号、拍摄时间等信息,业务上不需要,留着既增加体积也有隐私风险。压缩时统一剥离元数据,只保留方向信息用于摆正,摆正之后方向信息也可以去掉。这一步在巡检类系统里尤其重要,因为照片可能包含现场位置的精确坐标。
剥完之后体积还能再降一到三成,具体看机型,有些手机塞进去的缩略图和厂商信息能有几百千字节,比缩略图本身还大。所以剥离要放在压缩之前做,先减负再转码,比反过来省一点。
列表页只用缩略图规格,详情页用中图,用户点开放大才加载大图。图片标签加上懒加载属性,滚动到可视区域才请求。这两条改完,首屏请求的图片数量从二十多张降到六张,带宽峰值跟着下来。
图片地址是内容相关的、不变的,所以可以设长缓存:文件名带规格标识,内容变了地址就变,不需要靠短缓存来保证新鲜度。边缘缓存命中率上到九成多之后,回源的流量只剩一成。这条规则和"地址带随机参数"正好相反,后者会让缓存全部失效,是流量浪费的常见原因。
流水线只处理新上传的图,历史几十万张要单独刷。做法是跑批任务,按时间倒序处理,先刷访问频次高的,再刷长尾。跑批要限速,避免把压缩服务打满影响实时上传。我们跑了十天,期间实时链路的延迟没有明显上升。
并发开的是四,一次取两百个任务,跑十分钟停两分钟,把窗口让给实时上传。十天刷了三十多万张,压缩服务的内存稳在两吉字节以内。开跑前先抽十张比对尺寸和体积,确认规格没配错再放开,不然刷完才发现问题,重刷一遍更费时间。
压缩做完,前端也要跟着改:列表页去掉原图地址,换成缩略图;导出功能单独走原图下载,不要复用详情页的地址。这一层容易漏,开发习惯了复制粘贴图片地址,改完压缩没改前端,等于白做。我们的做法是在接口层就把规格固定下来,列表接口只返回缩略图地址,从源头上避免用错。
巡检系统七成流量来自手机,移动端有两件事要单独处理。一是上传前先在手机上压一遍,把十几兆的原图压到一两兆再传,省掉上行流量,也减少后台的处理压力;二是弱网下的断点续传,车间的信号不稳定,传一半断掉的情况每天都有。前端压缩用画布就能做,成本很低,但它不能替代服务端压缩,因为客户端可以绕过。
处理量和失败率我们放在搭贝的一张看板上盯,按小时聚合,哪一批任务失败了、卡在哪个规格,点进去就能看到。这套看板本身很简单,但它让一次偶发的失败不至于拖到用户投诉才被发现。
上线一个月,图片相关的出网流量降了五成多,列表页在车间的打开时间从五秒多降到一秒左右。上行流量也降了,因为手机端先压了一道,同样的巡检记录,上传耗时从二十多秒缩到五六秒,工人愿意在现场就提交,而不是攒到回办公室再补录。存储侧也受益:虽然多存了三份规格,但总占用只涨了两成,因为每份都小。用户的反馈很直接,说现在敢在车间里刷巡检记录了,以前要走到办公室连着无线才敢点。
图片压缩流水线的要点是四条:异步处理不阻塞上传、规格按场景定、参数用真机对比选、元数据剥离。配套要做的是长缓存、懒加载、前端按规格取图。这套东西的收益很确定,投入也不大,一个后台任务加一张规格表就能跑起来,剩下的都是参数调整的耐心活。参数没有标准答案,只能拿自己的业务图片去试,试出来的那一组才是对的。我们前后调了三轮,每轮抽二十张典型图片,在手机和电脑上各看一遍,才把三个规格的质量值定下来。
问:压缩质量设多少合适,有没有通用值?
答:没有通用值,要按场景对比。缩略图七十上下足够,详情页八十上下,原图不做有损。判断方法不是看数字,而是拿真实业务图片压出来,在目标设备上看:如果肉眼看不出差别而文件小了一半,这个值就合适。不同内容的图片敏感度差别很大,文字多的截图需要更高质量,而风景照片可以压得更狠一些。
问:压缩会不会把照片里的文字压糊?
答:会,尤其是巡检单、铭牌这类需要看清文字的图。处理办法是识别出文字密集的图片,单独走高质量规格,或者干脆用无损格式。判断方式可以是图片的方差和边缘密度,也可以是业务字段标记。宁可多存一份大的,也不要让关键信息看不清。先定场景,再定数值,顺序颠倒会白调,值得花半天先想清楚。
问:多规格存储会不会很占空间?
答:总占用通常比想象中低。三个规格加起来往往不到原图的七成,因为压缩率很高。如果空间紧张,可以只保留缩略图和中图,大图按需从原图实时生成并缓存。这种按需生成的方案能省存储,代价是首次访问慢一点。
问:历史图片太多,回刷要注意什么?
答:三点。限速,别和实时上传抢资源;可回滚,处理前保留原图,处理失败能退回;可观测,记录每批的处理量、失败率、体积变化,出问题能定位到哪一批。刷的过程中不要改规格矩阵,否则同一批图片会生成不一致的结果。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。