从LCP到图片压缩:电商商品图智能优化全复盘

从LCP到图片压缩:电商商品图智能优化全复盘 一次大促前的压测把我们的首页老底翻了个彻底4G 网络下首屏平均要 6.8 秒才能稳定运营说页面卡得没法看后端接口却都在 200ms 以内。真正把时间吃掉的是商品图片——12 个商品卡将近 9MB 的图片资源一半以上是原图直出或者是设计师手工导出的超大 JPG。这次之后我开始系统性地做零售商品图片的智能压缩与优化核心目标只有一个在不牺牲视觉观感的前提下最大限度提升页面加载速度。这篇技术实践分享就是这次改造从原理、选型、落地到后期排坑的完整复盘适合电商前端、后端、性能优化工程师以及所有被“图片拖慢页面”折磨过的人。1. 商品图到底有多重零售场景里的性能账本1.1 一个 SKU 不只是一张图很多非电商背景的工程师对商品图的体量是没有概念的。以为图片优化就是找几张 banner 压缩一下实际上零售商品图是一个典型的“海量小文件”场景而且数量级远超想象。一个普通 SKU 通常包含这些图片白底主图、模特图、场景图、细节特写、包装图、尺码表有的还有 360 度展示图。单张摄影棚原图轻松到 3-8MB一个 SKU 积累下来 20-30MB 很常见。如果店铺有几千个 SKU再加上历史版本、活动素材、不同运营小组重复上传的素材图片服务的存储量和访问量都会迅速膨胀。这带来的第一个问题就是存储成本第二个问题才是页面性能。很多团队会把注意力放在接口优化、首屏 JS 拆包上结果一查网络面板发现图片字节数占比常常超过 70%。接口再快用户的眼睛还是被图片下载时间卡住。1.2 页面加载速度的瓶颈为什么轮到图片这里有个很反直觉的点图片不像 JS 和 CSS它没法做常规的文本压缩。HTTP gzip 对图片基本无效因为图片编码以后已经是高度压缩的二进制数据再压一遍很难有明显收益。而且图片还有一个特性是“必须整张下载完才能渲染出一部分”虽然现代浏览器支持渐进式 JPEG但大部分场景下一张大图就是 LCP 的绝对瓶颈。LCP也就是 Largest Contentful Paint指的是用户可见区域内最大元素的渲染时间。在零售页面里这个最大元素十有八九是商品图或者运营 banner。我们当时用 Lighthouse 跑了一遍商详页LCP 元素就是轮播图里的第一张商品主图那张图原图 2.8MB光下载就花了 1.8 秒。所以只要图片不优化其他前端优化做得再好用户感知到的加载速度还是慢。首屏性能问题本质上是图片性能问题。1.3 先定预算再行动做图片优化最忌讳的就是“拍脑袋压缩”。没有量化目标压出来的图到底是好是坏、是否达标全凭感觉。我们在项目启动时先定了一套图片体积预算图片位置建议预算说明列表页缩略图≤ 60KB搜索结果、商品卡用尺寸通常 240-400px商品卡图片≤ 150KB首页推荐位、分类页商品卡商详页轮播主图≤ 300KB商详首屏最重要的视觉区域活动 banner≤ 200KB运营直接投放容易失控长图详情页≤ 800KB允许纵向滚动但要控制整页图片总字节定完预算再把这些预算写进 Lighthouse CI 或性能监控平台。之后每次压缩、每次图片上线都拿预算来卡而不是靠肉眼反复看。2. 压缩的本质人眼感知和编码器之间的那点交易2.1 一张位图是怎么算出来的先做个简单的数学题。一张 2000×2000 像素的 RGB 图片在完全没有压缩时体积是 2000 × 2000 × 3 字节算下来接近 12MB。如果带有透明通道也就是 RGBA那就是 16MB。所以图片能不能压缩、能压多少取决于你到底能去掉多少“冗余”。冗余分两种。一种是空间冗余比如白底商品图的背景是一片纯白一个像素和周围一万个像素完全一样另一种是视觉冗余也就是人眼根本感知不到或者不敏感的细节。有损压缩主要针对的是后者这也是压缩率能远超 zip 这类无损算法的核心原因。2.2 有损压缩在“骗”眼睛现代有损图片编码器做的事情可以简化成四步。第一步把 RGB 颜色空间转换成 YCbCr也就是把亮度信息和色度信息分开。人眼对亮度的敏感度远高于色度分开以后就可以区别对待。第二步是色度子采样常见的有 4:4:4、4:2:2、4:2:0。4:4:4 表示色度不降采样4:2:0 表示色度在水平和垂直方向都只保留 1/4对文件大小的影响非常明显。第三步是频域变换和量化比如 JPEG 里的 DCT把图像从像素域变成频率域然后把高频细节按照量化表做除法取整。人眼对高频细节的容忍度高所以这一步可以去大量数据。最后是熵编码把前面得到的数据流做最终压缩。用个生活化类比无损压缩像把一件羽绒服抽真空压完还是那件羽绒服有损压缩像是把羽绒服里的绒换成普通棉花穿上大概暖和但蓬松度和质感已经变了。压缩器的目标就是换一种“看起来差不多”的东西把体积降下来。2.3 JPEG、WebP、AVIF 谁更能打零售图片优化绕不开格式选型。现在主流的格式有三种格式相同主观质量下的体积透明通道兼容性主要特点JPEG基准不支持极好老旧但生态成熟编码速度快WebP约为 JPEG 的 60-75%支持很好有损无损都行解码器普及率高AVIF约为 JPEG 的 40-55%支持一般压缩率最高但编码慢老系统兼容性有限JPEG XL与 AVIF 接近或更好支持较弱新格式目前适合实验性使用我个人的经验是不要意气用事一上来就全站切 AVIF。AVIF 压缩率高但编码耗时会明显增加而且部分低端安卓 WebView 解码 AVIF 反而会出现卡顿。生产环境最稳的组合是“AVIF 优先 WebP 过渡 JPEG 兜底”通过picture标签或服务端 Accept 协商来做降级。2.4 质量参数不是拍脑袋填的每个编码器都有一堆参数quality只是其中一个。JPEG 的 quality 控制量化表强度WebP 除了 quality 还有 effort控制编码器花多少时间去寻找更优压缩方案AVIF 有 crf、speed、quantizer 等概念。一定要理解“质量参数和主观视觉质量不是线性关系”。从 q90 降到 q80文件可能缩小 40%肉眼几乎看不出区别从 q50 降到 q40文件也许只缩小 10%但画质已经开始崩了。所以最佳实践是找自己商品图类型的“拐点”而不是照搬网上某个标准参数。后面我会讲我们是怎么自动化找这个拐点的。3. 选型评估本地工具、图像库还是图片处理服务3.1 先判断你的图片是“一次性”还是“持续性”技术选型之前先回答一个问题你的图片是历史存量居多还是每天都会持续上新如果只是把一批历史图片压一遍那根本不需要写复杂系统。Squoosh、cwebp、avifenc 这些工具足够用手动跑几个命令就行。但零售电商是每天都有新商品、新素材的场景压缩必须变成一条自动化流水线而不是一次性的脚本。否则三个月后线上又会重新堆满大图。如果图片是在用户请求时才动态生成的比如用户上传一张头像或店铺装修图那就得考虑实时处理方案或者上传时异步转码。商品图不一样商品上架前是可以接受离线批处理的所以我们选择预生成。3.2 常用工具横向对比我实际用过的工具和方案主要有这些工具类型优点需要注意的点cwebpGoogle 官方 CLIWebP 参数控制细致稳定只处理 WebP需要自己写批量脚本avifencAVIF 官方参考编码器压缩率上限高编码慢参数复杂libvipsC 库内存占用极低适合大批量需要学习和封装sharpNode.js 的 libvips 绑定API 友好支持 WebP/AVIF对 Node 版本有要求Pillow-SIMDPython 图像库Python 生态好JPEG 有 SIMD 优化AVIF 支持不如 sharp 方便ImageMagick通用 CLI格式全命令广大批量处理时内存占用偏高云厂商图片处理SaaS接入快有 CDN 配合按量计费单价需要考虑3.3 为什么最终选了 sharp我们的技术栈偏 Node.js而且需要同时输出 WebP 和 AVIF所以最终选了 sharp。它对 libvips 的封装比较完善处理大图时内存控制很好官方文档里给出的性能数据也足够优秀。没选纯云服务的原因也很简单我们的商品图本来就存在自建的对象存储里访问走 CDN。如果引入云图片处理服务等于把整条链路重新接了一遍还要考虑每个月的调用费。而自建一条预生成流水线只是多一个脚本、一个任务队列的事。如果你团队人力紧张或者图片访问量不稳定直接买云服务其实更划算。判断标准就一条算一下一个月要转码多少张图云服务费用是否超过一个初级工程师一天的人工成本。图片量小云服务图片量大且规则复杂自建。4. 实操构建一条商品图智能压缩流水线4.1 “智能”到底体现在哪里所谓智能压缩不是简单地对所有图片跑同一个 quality。商品图内容差异很大白底图、模特图、皮革纹理图、透明 PNG它们对压缩参数的敏感度完全不同。我们做的第一件事是提取图片特征主要看三个维度尺寸超过目标尺寸多少倍先缩后压。透明通道有 alpha 的图不能输出 JPEG。复杂度用通道标准差做了一个粗略估计。背景纯白、物品居中的白底图标准差很低复杂纹理或密集商品的图标准差高。根据这三个维度我们设定了几套策略低复杂度图用低质量、高压缩高复杂度图提高 quality防止边缘发糊带透明通道的图走 WebP 或 PNG需要保留文字的类目图比如电子产品参数图则使用近无损模式。4.2 核心代码实现下面这段代码是我们流水线的简化版用 sharp 完成多尺寸输出和格式转换。const sharp require(sharp); const fs require(fs); const path require(path); async function optimizeProductImage(input, outputDir) { const metadata await sharp(input).metadata(); const stats await sharp(input).stats(); // 取通道标准差的峰值粗略判断图像复杂度 const complexity Math.max(...stats.channels.map((c) c.stdev)); const baseOptions { quality: 78, effort: 4 }; if (complexity 60) baseOptions.quality 84; if (complexity 35) baseOptions.quality 72; const sizes [ { name: thumb, width: 240 }, { name: list, width: 600 }, { name: detail, width: 1200 }, ]; for (const size of sizes) { await sharp(input) .resize(size.width, null, { withoutEnlargement: true }) .webp(baseOptions) .toFile(path.join(outputDir, ${size.name}.webp)); // 透明图不输出 AVIF避免兼容性问题 if (!metadata.hasAlpha) { await sharp(input) .resize(size.width, null, { withoutEnlargement: true }) .avif({ quality: Math.round(baseOptions.quality * 0.85), effort: 5, }) .toFile(path.join(outputDir, ${size.name}.avif)); } } } async function batchRun(inputDir, outputDir) { const files fs.readdirSync(inputDir).filter((f) /\.(jpe?g|png)$/i.test(f)); for (const file of files) { const src path.join(inputDir, file); const name path.parse(file).name; const out path.join(outputDir, name); fs.mkdirSync(out, { recursive: true }); await optimizeProductImage(src, out); console.log(done: ${file}); } }这段代码有几个关键点第一stats.channels返回的是各个通道的均值和标准差标准差大说明图像的边缘和纹理丰富需要更高 quality。第二withoutEnlargement: true很重要避免 300px 的小图被强行放大成 1200px。第三AVIF 的 quality 比 WebP 低 10-15 个百分点是正常的因为两者算法不同不能拿同一个数去套。4.3 命令行兜底方案有时候运营发来一张图不想走整套流水线命令行处理是最快的cwebp -q 80 -m 6 -pass 10 -metadata none input.jpg -o output.webp avifenc input.jpg output.avif -q 40 -s 4 --depth 10 -y 420-m 6是告诉 cwebp 用更慢但压缩率更高的模式-pass 10是让编码器多轮优化。AVIFENC 的-s 4是速度档位数字越大越快但压缩率越低-y 420是启用 4:2:0 色度子采样。需要注意的是命令行工具适合手工处理不适合大批量因为单张耗时不可控。4.4 质量校验和回归测试压缩完不能直接上线必须加一道质量校验。我们的做法是维护一个“黄金图片集”包含典型的白底图、模特图、透明 PNG、复杂纹理图。每改动一次压缩参数就重新跑一遍黄金集用compare -metric SSIM对比处理前后的结构相似度低于阈值就自动报错。另外自动化流水线里一定要加体积断言处理后的图如果比原图还大说明原图本身是伪装的 PDF 或者已经压过的图需要告警而不是默默通过。4.5 和前端/CDN 的衔接后端产出多格式文件后前端用picture标签做优雅降级picture source typeimage/avif srcset/img/1001/detail.avif source typeimage/webp srcset/img/1001/detail.webp img src/img/1001/detail.jpg loadinglazy decodingasync width1200 height1200 alt商品名称 /picturewidth和height一定要写否则图片加载完页面会发生布局偏移CLS 指标会很难看。loadinglazy让非首屏图片延迟加载decodingasync避免图片解码阻塞主线程。5. 上线后的量化收益从 Lighthouse 到商详页真实耗时5.1 用什么指标衡量我们的衡量体系分成两层实验室内 Lighthouse 数据以及真实用户环境的 RUM 数据。Lighthouse 侧重可复现的基准测试适合在发版前做横向对比。RUM 数据则反映了真实网络、真实设备下的表现。我建议重点盯四个指标FCP、LCP、CLS以及页面总传输字节数。图片优化最直接的影响就是 LCP因为它往往是最大图片元素。5.2 我们这批商品图的实测结果以下是一批 20 张典型商品图在改造前后的平均数据场景原图平均体积WebP 处理后AVIF 处理后LCP 变化首页商品卡1.8MB310KB220KB4.2s - 2.6s搜索列表图2.4MB380KB260KB首屏请求字节减少 76%商详轮播图8.5MB5张1.4MB5张1.0MB5张3.8s - 2.1s必须说明这是在我们设定的质量参数下测得的数据。如果你把 WebP 的 quality 调到 95体积不会下降这么多。我们把“肉眼不可见差异”作为参数选择的基准而不是追求极限压缩率。5.3 业务指标的观察图片变小之后最直观的感受是弱网测试环境不再卡成 PPT。我们做了一次持续一周的灰度实验控制其他版本变动只切换图片处理逻辑。实验组弱网用户的跳出率明显下降平均浏览页数略有上升。但我不会直接把转化率提升归因到图片优化上。电商业务的转化率受价格、库存、运营活动影响很大一次改版里通常混着多个变量。图片优化的价值更多体现在“体验成本”的降低搜索页、分类页这些高 PV 页面的用户能更早看到商品内容这是确凿的收益。6. 容易被忽略的四个后期坑缓存、源图质量、重复压缩和 CDN 回源6.1 缓存策略与版本号图片压缩完成后最容易被忽视的是缓存策略。如果图片 URL 没变CDN 和浏览器都会继续给用户旧图压缩成果要等缓存过期才能生效。我们的做法是给图片 URL 加上内容哈希压缩参数变更后文件名跟着变化从根源上避免缓存穿透。同时 CDN 上图片缓存时间设置得很长比如 30 天因为预生成图片已经不可变了。如果你使用了带 Accept 协商的动态格式还要特别注意 CDN 是否会把Vary: Accept纳入缓存 key否则就会出现 Chrome 拿到 AVIF、老浏览器也拿到 AVIF 的兼容性问题。6.2 源图质量决定一切这条经验是用事故换来的。有一批商品图我们压缩后发现文字边缘全是锯齿怎么调参数都没用。最后排查发现运营上传的所谓“原图”已经是别人压缩过两轮的 JPG质量参数只剩 55。在垃圾源图上做优化等于从井里提水水是脏的过滤器再强也没用。正确的做法是在存储层保留一份最优质量的 master 副本所有派生图一律从 master 生成。master 可以是高质量 JPEG、PNG 或 TIFF它不直接面向用户只作为压缩流水线的输入。有了 master之后想调整格式、尺寸、压缩算法都能随时重新生成。6.3 预生成 vs 实时处理预生成适合 SKU 可控的商品图但有些场景必须实时处理比如用户上传的图片、运营临时创建的营销素材。实时转码的代价是 CPU一张 2MB 的图在服务端转成 WebP 需要几十到几百毫秒高峰期会吃掉大量算力。我们的折中方案是热销商品图预生成长尾图片和实时上传图走异步任务队列先生成一个 600px 的临时缩略图再在后台慢慢生成完整规格。这样用户不会因为转码排队而白等。6.4 兼容性回退与客户端解码开销AVIF 不是所有浏览器都能解iOS 14 以下基本无缘。我们线上实际跑下来还有一类低端安卓手机虽然 WebView 支持 AVIF但解码一个 1200px 的 AVIF 图需要 100-200ms比解码同样尺寸的 WebP 慢不少。图片文件是小了用户等待时间反而没降。所以现在我们对低端机策略是根据Accept头里的格式偏好给客户端返回合理格式同时保留 JPEG/WebP 作为降级。不要在兼容性上赌零售用户的设备千奇百怪稳定比技术新潮重要。6.5 最后一条经验先缩尺寸再谈压缩压缩做得再狠也不如 resize 一刀来得有效。4000px 的摄影大图哪怕压到 200KB扔到 300px 的列表缩略图里依然是浪费流量。也就是说图片优化的第一原则永远是“匹配容器尺寸”第二原则才是压缩算法。我们的流水线里resize 永远在编码之前并且每个输出规格都有明确的宽度上限。经过这轮实践我最大的感受是图片优化不是做一个一次性项目而是要把“先分析、再编码、后校验”变成一条自动运转的管道。只要 master 源图和管理规范在后续换编码器、调参数、加新规格都只是在这条管道上加一段逻辑而已。