ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

图片优化实战:从格式选型到加载策略,页面加载速度全面提升

图片优化实战:从格式选型到加载策略,页面加载速度全面提升 图片优化做完之后页面加载速度到底能不能上一个台阶答案是能。我接手过的项目里图片往往占页面总字节量的一半以上。一个看似简单的 Banner 图可能就有几百 KB加载慢、占带宽还会拖累首屏渲染。做 Web 性能优化如果只调代码、砍脚本却不碰图片那基本等于没做。这篇博文就围绕图片优化这条线展开从图片格式选型、压缩工具链、加载策略到工程化落地和性能度量把我在实际项目里用过的方案和踩过的坑一次性说清楚。适合正在做 Web 项目性能优化的前端工程师也适合一个人撑起整个网站开发、想给页面提速但又不知道从哪里下手的全栈开发者。内容偏实操每部分都能直接拿到项目里用。1. 先想清楚图片优化到底在优化什么1.1 图片为什么成了 Web 性能的头号瓶颈先看一组我自己测过的数据。一个典型的电商列表页页面总字节数约 2.1MB其中图片占了 1.6MB占比超过 76%。这还是在我已经做了基础压缩的前提下。要是完全没有优化图片占比只会更夸张。图片拖慢性能的原因不止“文件大”这一个至少有三个层面第一是传输层面。文件越大在网络传输上花的时间越多尤其是在弱网环境下。同样是 500KB 和 100KB 的图片在 4G 网络下加载时间可能相差几百毫秒在 2G 网络下差距可能是秒级的。对移动端用户来说这个差距直接决定了他们会不会关掉你的页面。第二是解析层面。浏览器下载完图片后需要解码才能渲染。大图解码消耗的是 CPU 资源特别是在低端手机上一张 4000 x 3000 像素的图片解码可能要花几十毫秒甚至上百毫秒。如果首屏有大量图片同时解码主线程会被严重阻塞页面滚动都卡。第三是布局层面。如果没有给图片预留尺寸空间图片加载完会导致布局跳动也就是我们常说的 CLSCumulative Layout Shift问题。这种体验上的问题用户可能说不清哪里不对但就是觉得你的网站“不稳”。1.2 图片优化的本质是一条链路不是一个动作很多人以为图片优化就是“用 Photoshop 把图缩小一点再导出”。这确实有效但远远不够。图片优化应该看作一条完整的链路每个环节都能榨出性能也会互相影响源头选型选什么格式直接决定了文件大小的天花板。压缩处理质量参数、编码方式、元数据这些细节决定文件大小能压到什么程度。加载策略什么时候加载、加载哪个尺寸决定用户实际需要等待多久。缓存策略图片能不能被浏览器或 CDN 缓存决定第二次访问的速度。指标度量优化的效果是否真实反映到 LCP、CLS、带宽消耗这些数字上。我之前遇到过这样一个项目开发团队已经很认真地压缩了图片每张都压到了看起来很小的大小但首屏加载还是很慢。排查之后发现问题出在加载策略上——首屏下方好几屏之外的图片也同步加载了。也就是说做图片优化不能只盯着“文件大小”这一个维度加载时机同样关键。只看文件大小或者只看加载策略都是偏科。2. 图片格式选型不是越新越好是合适才好2.1 JPEG、PNG、GIF老格式的适用边界老牌格式到现在还没退役是因为它们在特定场景下有不可替代的优势。JPEG适合照片、渐变丰富的复杂图像。它是有损压缩压缩率高但对文字、线条、边缘锐利的图形不友好压狠了会出现严重的蚊状噪点。PNG适合需要透明背景的图片、图标、界面截图。无损压缩保证了边缘清晰但文件大小通常比 JPEG 大不少。一张简单的 PNG 图标可能比同等视觉效果的 WebP 大 3 到 5 倍。GIF只支持 256 色唯一还在用它的理由是简单动画。但文件体积和画质都不理想现在做动画优先考虑 WebP 或视频。举个实际例子如果有个产品详情页需要展示商品图用 JPEG 就很合适如果是 app 界面截图需要保留每一个像素细节和文字锐度用 PNG 更靠谱。有人不管三七二十一把所有图片都转成 WebP结果界面截图里的文字出现毛边用户反馈看不清楚这就属于选型没考虑清楚。2.2 WebP 和 AVIF现代格式到底强在哪WebP 是 Google 在 2010 年推出的格式支持有损、无损、透明和动画。我在 2020 年前后大规模改造项目时把主要图片从 JPEG 转到 WebP平均体积降低了 30% 到 50%。无损场景下WebP 通常比 PNG 小 20% 到 40%而且支持透明度所以很多 PNG 图标可以无损地转成 WebP。AVIF 是更新一点的格式基于 AV1 视频编码技术压缩率比 WebP 更高。我实测过一组电商主图格式单张平均大小画质主观评价JPEG (质量 75)180KB能接受边缘略有噪点WebP (质量 75)105KB清晰几乎无可见损失AVIF (质量 50)68KB略有一点涂抹感细节少一些这组数据说明AVIF 能压到 JPEG 三分之一左右的体积但它的 CPU 解码成本更高低端设备上解码 AVIF 会比解码 WebP 慢不少。所以 AVIF 也要看场景并不是无脑上。2.3 我现在的选型策略基于这些年的使用经验我的策略可以总结成一句话越重要的图片越要用最合适的格式而不是用最通用的格式。具体选型逻辑我整理成了下面这个流程项目里需要确定图片格式时直接套用图片包含透明通道且需要边缘无损如 Logo、图标优先 WebP兼容性没问题的话不用 PNG。照片、实拍图、商品图优先 AVIF降级到 WebP最后兜底 JPEG。简单动画如 loading 动画、图标动画优先 WebP 动态图必要时退回到 GIF。界面截图、含大量文字的图用 PNG 或 WebP 无损模式别用高压缩的 JPEG 或 AVIF文字会糊。这里要注意一个兼容性问题WebP 在现代浏览器上已经是全绿状态AVIF 对较老的浏览器比如 2020 年前的版本支持不完整。所以用 AVIF 时一定要提供降级方案我会在后文讲 loading 策略时说到 picture 标签的用法那个就是解决“浏览器不支持也能显示”的标准手段。3. 压缩工具链把同一个动作标准化、自动化3.1 画图工具手动导出 vs 命令行批量处理我刚入行时做图片优化用的是 Photoshop “存储为 Web 所用格式”一张一张地导出。图片少还行几十张图就累死人了更别说一整个电商网站几千张图。手动处理还有一个很大的隐患不同人导出的质量参数不一样有人用 80有人用 60最终页面上图片画质参差不齐。后来我把整个流程迁到了命令行工具用脚本批量处理。好处有三条统一参数所有人、所有图片都用同一套压缩规则不会因为个人习惯导致差异。可重复脚本跑一遍压缩结果是一致的改参数也只需要改一处。可集成可以直接挂在 CI/CD 流程里代码提交时自动压缩图片不需要人肉操作。如果你还没有接触过命令行图片处理我建议从两个工具入手。第一个是ImageMagick老牌全能选手功能强大命令简单适合临时处理。第二个是sharp针对性能设计的 Node.js 库底层是 libvips处理速度快、内存占用低适合集成到前端工程中。3.2 sharp 实战一个脚本搞定批量压缩sharp 是我现在的主力工具。下面这个 Node 脚本可以把一个目录下的所有图片统一转成 WebP 并压缩const sharp require(sharp); const fs require(fs); const path require(path); const inputDir ./originals; const outputDir ./optimized; if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } fs.readdirSync(inputDir).forEach(async (file) { const ext path.extname(file).toLowerCase(); if (![.jpg, .jpeg, .png, .webp].includes(ext)) return; const inputPath path.join(inputDir, file); const outputPath path.join(outputDir, ${path.basename(file, ext)}.webp); try { await sharp(inputPath) .resize({ width: 1200, withoutEnlargement: true }) .webp({ quality: 78, effort: 4 }) .toFile(outputPath); console.log(优化完成: ${file}); } catch (err) { console.error(处理失败: ${file}, err.message); } });这个脚本做了三件事把图片统一限制宽度为 1200px超出的缩小不超出则保持原尺寸。这一步能避免页面里出现超大原图。转成 WebP质量设为 78。这个质量值是我在画质和体积之间反复测试后选出来的绝大多数场景下看起来和原图几乎无差别。effort 参数设为 4它控制的是压缩耗时和压缩率的权衡取值 0 到 64 是平衡点压缩率比 0 高不少耗时又不至于太久。对于需要保留透明通道的 PNG 图标我会另外写一个分支用sharp(...).webp({ lossless: true })来压缩。直接套用有损压缩会导致透明边缘出现一圈杂色这是透明图转 WebP 最常见的坑。3.3 质量参数怎么定不要只搜“推荐值”要自己测网上能搜到一堆“推荐质量值”什么 JPEG 80、WebP 75但它们基本都是别人项目的经验未必适合你的图片内容。我踩过的一次坑把一组人物摄影作品压到质量 70结果人脸的皮肤纹理出现明显色块用户截图来投诉。后来改成质量 85体积只多了 10% 左右但画质问题解决了。我的经验是每换一批图片内容至少要花十分钟做对比测试。具体做法很简单挑两三张有代表性的原图。分别用质量 60、70、80、85 导出。放在同一个页面上并排对比最好在真实设备上视网膜屏和普通屏都要看一眼。找到“肉眼几乎看不出区别体积又是最小”的那个档位。另外还有一个很容易被忽略的点移除元数据。相机拍摄的 JPEG 自带 EXIF 信息包括拍摄参数、GPS 位置、设备型号等这些信息跟网页展示毫无关系却占了几十 KB。前面脚本里没有显式处理但 sharp 默认会去掉大部分元数据。如果你用的是其他工具记得检查有没有移除元数据的选项。3.4 渐进式 JPEG被低估的加载体验优化JPEG 有两种编码方式基线式baseline和渐进式progressive。基线式是一行一行地渲染从上往下慢慢显示渐进式是先显示一个模糊的完整轮廓再一步步变清晰。渐进式 JPEG 的优势在弱网环境下特别明显用户看到的是一个逐渐变清晰的图而不是从空白慢慢刷出上半截感知等待时间会显著降低。代价是解码耗时稍高文件体积也可能略微增加通常在 5% 以内。我做过对比一张 150KB 的基线式 JPEG 转成渐进式后是 158KB体积多 5% 左右但体验提升明显这笔账很划算。snappy如果你用 sharpJPEG 默认就是渐进式的不需要额外设置。用其他工具时找一下interlace或progressive相关的选项。4. 加载策略把图片在正确的时间以正确的尺寸送达4.1 懒加载别把首屏下面的图片提前搬上来我在 1.2 节提到的那个案例就是典型的懒加载缺失问题。页面虽然压缩了图片但首屏下面的十几张图片全部同步加载浏览器一口气请求了几十张图带宽被占满首屏真正需要的图片反而排队。现在浏览器原生支持懒加载不需要引入任何 JavaScript 库img srcproduct.jpg loadinglazy alt商品图loadinglazy告诉浏览器图片进入视口附近时才加载。现代浏览器对这个属性的支持已经很好而且它会自动处理“快到视口就开始加载”的预加载距离不会出现用户快速滚动时图片还没加载好的情况。不过要特别提醒首屏关键图片不能加 lazy。比如页面第一屏的主视觉 Banner、核心商品图这些是用户第一眼就要看到的内容加了 lazy 反而可能延迟加载。正确做法是首屏图片保持默认立即加载首屏以外的图片才使用 lazy。另外配合decodingasync可以让图片解码不阻塞其他内容的渲染img srcproduct.jpg loadinglazy decodingasync alt商品图decodingasync告诉浏览器“图片可以异步解码”这样图片解码时页面上的文字和其他元素能继续渲染对首屏渲染速度有帮助。4.2 srcset 和 sizes同一张图按设备下发不同尺寸移动端和桌面端的屏幕宽度完全不同。一张 1920px 宽的 Banner 图在手机屏幕上显示时实际可能只有 750px 宽直接下发原图就是浪费流量。响应式图片的方案是让浏览器自己根据视口宽度选择合适的图片源img srcbanner-750.jpg srcsetbanner-750.jpg 750w, banner-1280.jpg 1280w, banner-1920.jpg 1920w sizes(max-width: 750px) 750px, (max-width: 1280px) 1280px, 1920px alt主视觉 Banner 这里的750w、1280w表示图片的实际宽度sizes告诉浏览器这张图在某个视口宽度下实际会被渲染成多宽。浏览器会根据屏幕宽度、设备像素比DPR和 sizes 自己决定下载哪张图。我第一次用 srcset 时犯过一个错误没有写 sizes只写了 srcset。浏览器的行为就变成“一律按视口宽度 100% 来猜图片渲染宽度”结果在手机上的里选图逻辑完全不对——它选了最大的图。后来加上 sizes 之后一切才正常。所以记住一句话srcset 负责提供候选图sizes 负责告诉浏览器“图实际占多宽”两个缺一不可。4.3 picture 元素真正解决格式兼容问题要使用 AVIF 又要兼容旧浏览器靠 srcset 还不够得用 picturepicture source typeimage/avif srcsetbanner.avif source typeimage/webp srcsetbanner.webp img srcbanner.jpg alt主视觉 /picture浏览器从上往下匹配遇到能解码的格式就加载都不支持就回退到 img 标签里的 JPEG。这个机制简单可靠是我在项目里标配的用法。注意 picture 内部的 img 标签不能省略它既是兜底图也是整个 picture 的尺寸基准。还有一个小细节source 里的图片文件路径一定要和 img 里的 alt 文案一起设计因为如果图片加载失败显示出来的 alt 文本就非常重要。做 SEO 时alt 文案本身也有权重不要为了偷懒就空着。4.4 LCP 图片的特殊待遇preload 优先加载LCPLargest Contentful Paint最大内容绘制是衡量页面加载体验的核心指标之一页面上最大的那块内容——通常就是一张大 Banner 图——决定了 LCP 的数值。如果这张图要等浏览器自己去发现、排队、下载LCP 可能就慢了。可以用 preload 告诉浏览器“这张图很重要提前拉起来”link relpreload asimage hrefbanner.avif typeimage/avif要注意两点。第一preload 只对 LCP 那一张图用不鼓励大面积使用因为 preload 会占用网络请求的优先级所有图都 preload等于没有 preload。第二如果 LCP 图片用了 picture 多格式切换preload 的 href 需要和实际加载的格式对应否则 preload 了 A 格式、浏览器实际加载 B 格式就浪费了一次请求。5. 工程化落地CDN、缓存与自动化改造5.1 图片 CDN让压缩和分发脱离本地工程到了这一步你可能已经用脚本批量压缩了图片但这还只是本地优化。一旦图片量级变大比如电商平台有几十万张商品图本地预处理就不太现实了需要把图片交给 CDN 处理。现在很多 CDN 服务商提供图片实时处理能力支持按 URL 参数动态调整格式、尺寸和压缩率。比如你可以构建这样一个 URLhttps://cdn.example.com/images/products/12345.jpg?w750q78formatwebpCDN 会根据参数实时返回压缩后的 750px 宽 WebP 图。这种做法最大的好处是前端不需要预先存好几套不同尺寸的图只需要存一张原图其他尺寸、格式都由 CDN 动态生成。同时 CDN 的边缘节点会把处理结果缓存起来用户访问时直接从最近的节点取图响应速度更快。选 CDN 时一定要关注三件事是否支持 WebP/AVIF 转换、是否有灵活的尺寸调整参数、国内节点的覆盖质量。即使是免费的业务也看看这些能力不然以后改造会很费劲。5.2 HTTP 缓存策略第二次访问为什么快了一倍图片文件本身就适合长时间缓存因为它们极少变化。理想策略是图片 URL 带上版本号或文件指纹然后配置长时间缓存。以 Nginx 为例location /static/images/ { expires 30d; add_header Cache-Control public, max-age2592000, immutable; }这里的关键词是immutable意思是“这张图片永远不会变”浏览器连重新验证都不会做直接用本地缓存。配合文件名里的哈希值只要文件内容变了URL 就变了浏览器自然去下载新的。如果你没有独立服务器用的是各种免费 web 服务器搭建的站点也可以通过 HTML 中的 meta 标签或 HTTP 响应头配置来设置缓存只是灵活度不如 Nginx。方案选型时务必评估你现有的基础设施能力。5.3 图标和背景图小图也有大讲究说完大图再说小图。页面上的小图标、小背景图数量多请求也多如果不合理处理也可能拖慢页面。单个图标体积小但数量多合并成雪碧图CSS Sprite把几十个小图标拼到一张大图上用 background-position 定位。这种做法能明显减少请求数量。缺点是维护麻烦新增图标要重新拼图。图标字体iconfont把图标做成字体通过 Unicode 引用。一次请求加载整组图标缩放不模糊。缺点是在某些浏览器上抗锯齿效果不如图片复杂图形表现不好。内联 SVG直接把矢量图写进 HTML没有额外请求且支持 CSS 控制颜色。非常适合少量、需要交互状态的图标。缺点是脚本和数据量大的时候会让 HTML 变大不建议大量使用。我的建议是项目初期就定好图标方案不要混着用。我见过一个项目雪碧图、iconfont、内联 SVG 三种方案同时存在每个页面要维护三套图标体系新增一个图标要在三个地方改。后来花了两个迭代把图标统一成内联 SVG代码量和维护成本都降下来了。5.4 自动化检查让优化效果不滑坡图片优化不是一锤子买卖。团队不断新增图片内容很容易又出现大体积图片所以我养成了一个习惯在 CI 流程里增加一个图片体积检查的脚本。流程大概是这样扫描本次提交涉及的图片。检查图片文件的体积和格式是否符合项目规范比如单张图不超过 150KB、主图格式为 WebP。超过阈值时输出 warning 甚至直接阻止合并。实际执行时我用过 GitHub Actions 结合 Node 脚本做的方案在 pull request 时自动跑检查给提交者反馈“这张图超标了”既有提示作用又不会太打扰日常开发。这种自动化的力量在于它把“性能规范”变成了“工程质量门禁”而不是靠某个人天天盯着。6. 指标度量图片优化有没有效果用数字说话6.1 用 PerformanceObserver 测量 LCP优化做完了很多人会凭感觉说“好像快了一点”但项目复盘需要的是数据。至少要从三个维度度量LCP首屏最大内容的渲染时间受图片影响最大。CLS布局稳定性和图片是否预留尺寸直接相关。传输字节数页面总大小和图片总大小。用 PerformanceObserver 可以拿到真实的页面性能数据new PerformanceObserver((list) { for (const entry of list.getEntries()) { if (entry.entryType largest-contentful-paint) { console.log(LCP:, entry.startTime); } } }).observe({ type: largest-contentful-paint, buffered: true });在开发环境里这个数据可以作为参考但因为开发环境网络条件好数值往往偏小。更重要的是上线后看两个地方一是浏览器自带的性能分析工具二是第三方监控平台的数据。移动端性能优化是否到位最终要看真实用户环境下的数据。6.2 给图片优化定一个“性能预算”性能预算这个概念已经提了很多年简单说就是先定好页面不能超过多大的体积再倒推图片最多占多少。我的参考值页面类型页面总预算图片预算单张主图上限移动端列表页1.5MB1MB150KB移动端详情页2.5MB1.8MB250KB桌面端官网首页3MB2.2MB350KB这些数值不是拍脑袋定的而是根据“4G 网络下 2 秒内完成首屏主要内容加载”的目标反推的。如果一个页面超过了预算优先检查图片因为图片通常是最容易“瘦身”的部分。定了预算之后每次改动前都对照一下养成习惯之后性能问题基本不会积压成上线前的突发状况。6.3 一个真实项目的优化对比最后用一个真实案例来收束。之前做的一个 B 端管理系统登录页和仪表盘页图片特别多包括背景图、团队头像、数据图表截图。优化前仪表盘页面总大小 1.8MB其中图片 1.4MBLCP 在 4G 模拟环境下是 2.8 秒。优化动作按顺序做了四件事所有背景图和头像转成 WebP体积降了 45%。首屏外图片全部加 loadinglazy首屏请求数从 23 个降到 6 个。背景图按设备尺寸用 srcset 下发不同大小移动端不再下载桌面版大图。给 LCP 背景图加了 preload并设置 CDN 差异缓存第二次访问速度明显提升。优化后页面总大小降到 812KB图片只占 520KBLCP 降到 1.4 秒。没改一行业务代码只动了图片。这就是图片优化在整体 Web 性能优化里的性价比——它不需要复杂的架构改造只需要把格式、尺寸、加载时机、缓存这四个维度都照顾到效果立竿见影。写在最后的经验之谈做了这么多年图片优化我最大的感受是不要试图追求单个环节做到极致而要把整个链路跑顺。图片压缩率再高加载策略不对用户还是等CDN 再快图片本身没优化带宽照样浪费。这四件事之间不是独立的关系而是一个整体。另外一个小技巧也是我最近一直在用的把图片优化融入日常开发习惯而不是等到性能变差了才想起它。每次新增图片时顺手看一眼格式和大小每次提交前跑一遍压缩脚本每次开会评审页面时顺便聊一下性能预算。这些动作单独看都不起眼但坚持三个迭代之后页面的性能表现会稳定在一个很高的水平而不是忽高忽低。图片优化不像一些新技术那样炫酷但它是最扎实、最容易被忽略、又最直接能改善用户体验的工作之一。如果你正在做一个用户说“打开很慢”的项目从图片开始大概率不会错。
返回列表