图片优化

概述

图片是网页内容的重要组成部分,研究表明图片可以占据网页总传输量的 50% 以上。一张未经优化的图片可能达到数 MB,下载这样一张图片的时间足以让用户放弃等待。更糟糕的是,如果 LCP(最大内容绘制)元素是一张图片,图片加载的快慢直接影响着用户感知到的页面加载速度。

图片优化的挑战在于多个维度的平衡:体积质量的平衡——更小的文件通常意味着更低的视觉质量;格式选择的复杂性——WebP、AVIF、JPEG、PNG、SVG 各有优劣,没有万能最优解;响应式需求——用户可能在 320px 宽的手机或 2560px 宽的显示器上访问同一个页面。

一个完整的图片优化策略涵盖了从图片格式选择、压缩配置、响应式图片实现,到 CDN 集成、加载策略优化的完整链路。本节将系统讲解这些内容,帮助你构建适合自己项目的图片优化方案。

目标

  • 掌握现代图片格式(WebP、AVIF)的特性与适用场景
  • 理解响应式图片的实现方案与 <picture> 元素
  • 学会配置图片 CDN 与自动化优化流水线
  • 掌握 LCP 图片的专项优化策略

知识体系

1. 图片格式选择

选择正确的图片格式是优化的第一步。不同格式有不同的压缩特性、支持的色彩范围和功能,选择不当会影响图片质量和体积。

格式对比

格式 压缩类型 透明度 动画 浏览器支持 适用场景
JPEG 有损 全部 照片、复杂图像
PNG 无损 全部 图标、截图、需要透明度
WebP 有损/无损 >96% 通用替代 JPEG/PNG
AVIF 有损/无损 >92% 高压缩率照片
SVG 矢量 全部 图标、Logo、插画

JPEG 使用有损压缩,压缩率高,适合照片等复杂图像,但不支持透明度和动画。PNG 是无损压缩,支持透明度,适合需要清晰边缘的图像(如图标、截图),但文件较大。WebP 在相同质量下比 JPEG 小 25-35%,同时支持透明度和动画,是目前最推荐的通用图片格式。AVIF 基于 AV1 视频编码,压缩率比 WebP 更高(同样质量下体积更小),但编码速度较慢。SVG 是矢量格式,文件极小且无限缩放,适合图标和简单图形。

格式选择决策

图片格式选择:
┌─────────────────┐
│ 是矢量图形吗?    │
└───┬─────────────┘
  Yes → SVG
  No ↓
┌─────────────────┐
│ 需要动画吗?      │
└───┬─────────────┘
  Yes → WebP(动画)/ AVIF
  No ↓
┌─────────────────┐
│ 是照片/复杂图像?  │
└───┬─────────────┘
  Yes → AVIF > WebP > JPEG
  No ↓
┌─────────────────┐
│ 需要透明度?      │
└───┬─────────────┘
  Yes → WebP > PNG
  No → WebP > JPEG

这个决策树只是一个起点,实际选择还需要考虑具体场景。比如对于用户头像这样的小图,PNG 可能是更好的选择,因为 JPEG 的有损压缩在低分辨率下会明显降低清晰度。

2. 图片压缩

构建时压缩

现代前端项目通常使用构建工具在打包时自动压缩图片:

// vite.config.js
import viteImagemin from 'vite-plugin-imagemin';

export default defineConfig({
  plugins: [
    viteImagemin({
      gifsicle: { optimizationLevel: 3 },
      optipng: { optimizationLevel: 7 },
      mozjpeg: { quality: 80 },
      pngquant: { quality: [0.65, 0.80], speed: 4 },
      svgo: {
        plugins: [
          { name: 'removeViewBox', active: false },
          { name: 'removeEmptyAttrs', active: true },
        ],
      },
      webp: { quality: 80 },
      avif: { quality: 50 },
    }),
  ],
});

构建时压缩在部署前处理所有图片,确保图片以最优体积部署到生产环境。质量参数的设置需要权衡:质量过高导致体积偏大,质量过低导致视觉失真。对于大多数场景,JPEG 80%、WebP 80%、AVIF 50% 是合理的起始点。

Sharp — Node.js 图片处理

Sharp 是 Node.js 生态中最强大的图片处理库,它使用 libvips 构建,处理速度极快。

import sharp from 'sharp';

// 基础转换与压缩
async function optimizeImage(inputPath, outputDir) {
  const image = sharp(inputPath);
  const metadata = await image.metadata();

  // 生成 WebP
  await image
    .webp({ quality: 80, effort: 6 })
    .toFile(`${outputDir}/image.webp`);

  // 生成 AVIF
  await image
    .avif({ quality: 50, effort: 6 })
    .toFile(`${outputDir}/image.avif`);

  // 生成多尺寸
  const sizes = [640, 960, 1280, 1920];
  for (const width of sizes) {
    if (width <= metadata.width) {
      await image
        .resize(width)
        .webp({ quality: 80 })
        .toFile(`${outputDir}/image-${width}w.webp`);
    }
  }
}

// 批量处理
import { glob } from 'glob';

async function processAllImages() {
  const files = await glob('src/images/**/*.{jpg,jpeg,png}');
  await Promise.all(files.map((file) => optimizeImage(file, 'dist/images')));
}

Sharp 特别适合自动化图片处理流水线。它可以同时处理单张图片和批量处理,支持多种格式转换、尺寸调整、裁剪等操作。effort 参数控制编码时的 CPU 投入,更高的 effort 意味着更好的压缩但更慢的编码速度。

3. 响应式图片

用户使用从 320px 到 2560px 不等宽度的设备访问同一个网站。响应式图片技术确保用户下载适合自己设备尺寸的图片,既不会因为图片过大浪费带宽,也不会因为图片过小而模糊。

srcset 与 sizes

srcset 属性允许你为同一张图片提供多个尺寸版本,浏览器会自动选择最合适的一个:

<!-- 基于宽度的响应式图片 -->
<img
  src="image-800w.jpg"
  srcset="
    image-400w.jpg  400w,
    image-800w.jpg  800w,
    image-1200w.jpg 1200w,
    image-1600w.jpg 1600w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 50vw,
    33vw
  "
  alt="响应式图片示例"
  loading="lazy"
  decoding="async"
/>

srcset 的语法是 图片URL 宽度描述符,多个版本用逗号分隔sizes 告诉浏览器这张图片在不同视口宽度下占用多宽。浏览器会根据 sizes 声明和设备像素比(DPR)计算出需要的图片宽度,然后从 srcset 中选择最接近但略大的那个。

picture 元素

<picture> 元素提供了更强大的格式协商能力:

<!-- 格式协商 + 响应式 -->
<picture>
  <!-- AVIF 格式(最佳压缩率) -->
  <source
    type="image/avif"
    srcset="image-400w.avif 400w, image-800w.avif 800w, image-1200w.avif 1200w"
    sizes="(max-width: 640px) 100vw, 50vw"
  />
  <!-- WebP 格式(广泛支持) -->
  <source
    type="image/webp"
    srcset="image-400w.webp 400w, image-800w.webp 800w, image-1200w.webp 1200w"
    sizes="(max-width: 640px) 100vw, 50vw"
  />
  <!-- JPEG 兜底 -->
  <img
    src="image-800w.jpg"
    srcset="image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w"
    sizes="(max-width: 640px) 100vw, 50vw"
    alt="多格式响应式图片"
    loading="lazy"
    decoding="async"
    width="800"
    height="600"
  />
</picture>

<!-- 艺术指导(Art Direction) -->
<picture>
  <source media="(max-width: 640px)" srcset="hero-mobile.webp" />
  <source media="(max-width: 1024px)" srcset="hero-tablet.webp" />
  <img src="hero-desktop.webp" alt="Hero image" />
</picture>

浏览器按顺序检查每个 <source> 元素,选择第一个支持的格式。如果浏览器支持 AVIF,就会使用 AVIF 源;否则继续检查 WebP。<img> 是最后的兜底,确保任何浏览器都能显示图片。

**艺术指导(Art Direction)**是 <picture> 的另一个重要用途——为不同屏幕尺寸提供不同裁剪的图片。比如桌面端显示横向全景图,移动端显示纵向裁剪后的重点部分。

4. LCP 图片优化

LCP(最大内容绘制)图片是页面加载性能的关键。优化 LCP 图片可以让页面更快地呈现主要内容。

<!-- LCP 图片专项优化 -->
<head>
  <!-- 预加载 LCP 图片 -->
  <link
    rel="preload"
    as="image"
    href="hero.webp"
    type="image/webp"
    fetchpriority="high"
    imagesrcset="hero-400w.webp 400w, hero-800w.webp 800w, hero-1200w.webp 1200w"
    imagesizes="100vw"
  />
  <!-- 预连接图片 CDN -->
  <link rel="preconnect" href="https://images.example.com" />
</head>

<body>
  <!-- LCP 图片:不使用 lazy loading,设置高优先级 -->
  <img
    src="hero-800w.webp"
    srcset="hero-400w.webp 400w, hero-800w.webp 800w, hero-1200w.webp 1200w"
    sizes="100vw"
    alt="Hero"
    fetchpriority="high"
    decoding="async"
    width="1200"
    height="600"
  />
</body>

LCP 图片优化的关键点:不使用 loading="lazy"——首屏图片必须立即加载;使用 fetchpriority="high"——告诉浏览器这是高优先级资源;预加载关键图片——<link rel="preload"> 让浏览器提前开始下载;预连接 CDN——减少连接建立时间。

5. 图片 CDN 与自动优化

图片 CDN 可以在运行时提供格式转换、尺寸调整等优化,减轻开发团队的负担。

Cloudinary / imgix 集成

// 图片 CDN URL 构建器
class ImageCDN {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  // Cloudinary 风格
  getUrl(imagePath, { width, height, quality = 'auto', format = 'auto' }) {
    const transforms = [
      `w_${width}`,
      height ? `h_${height}` : null,
      `q_${quality}`,
      `f_${format}`,
      'c_fill',
      'dpr_auto',
    ].filter(Boolean).join(',');

    return `${this.baseUrl}/image/upload/${transforms}/${imagePath}`;
  }

  // 生成 srcset
  getSrcSet(imagePath, widths = [400, 800, 1200, 1600]) {
    return widths
      .map((w) => `${this.getUrl(imagePath, { width: w })} ${w}w`)
      .join(', ');
  }
}

const cdn = new ImageCDN('https://res.cloudinary.com/demo');

// React 组件
function OptimizedImage({ src, alt, sizes, widths, priority = false }) {
  return (
    <img
      src={cdn.getUrl(src, { width: 800 })}
      srcSet={cdn.getSrcSet(src, widths)}
      sizes={sizes}
      alt={alt}
      loading={priority ? 'eager' : 'lazy'}
      fetchPriority={priority ? 'high' : 'auto'}
      decoding="async"
    />
  );
}

图片 CDN 的工作原理是在 URL 中包含转换参数,CDN 在请求时动态处理。format='auto' 让 CDN 根据请求浏览器的支持情况返回 WebP 或 AVIF;dpr_auto 根据设备像素比返回合适尺寸的图片。

6. SVG 优化

SVG 文件经常包含不必要的元数据、注释和空白,压缩后可以显著减小体积。

// svgo.config.js
module.exports = {
  plugins: [
    'preset-default',
    'removeDimensions',
    {
      name: 'removeAttrs',
      params: { attrs: '(fill|stroke)' },
    },
    {
      name: 'addAttributesToSVGElement',
      params: {
        attributes: [{ 'aria-hidden': 'true' }],
      },
    },
  ],
};
// SVG 作为 React 组件内联使用
// 避免额外的 HTTP 请求,支持 CSS 样式控制
function IconArrow({ className, size = 24 }) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      className={className}
      aria-hidden="true"
    >
      <path d="M5 12h14M12 5l7 7-7 7" />
    </svg>
  );
}

将小图标内联为 React 组件可以减少 HTTP 请求数量,同时允许通过 CSS 控制样式(如颜色、hover 效果)。对于大图标,还是应该作为独立文件加载。

7. 占位图与渐进加载

图片加载过程中的空白或布局跳动会影响用户体验。占位图技术可以改善这种状况。

LQIP(Low Quality Image Placeholder)

// LQIP(Low Quality Image Placeholder)方案
function ProgressiveImage({ src, placeholder, alt, ...props }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div style={{ position: 'relative', overflow: 'hidden' }}>
      {/* 低质量占位图 */}
      <img
        src={placeholder}
        alt=""
        aria-hidden="true"
        style={{
          filter: 'blur(20px)',
          transform: 'scale(1.1)',
          opacity: loaded ? 0 : 1,
          transition: 'opacity 0.3s',
          position: 'absolute',
          inset: 0,
          width: '100%',
          height: '100%',
          objectFit: 'cover',
        }}
      />
      {/* 完整图片 */}
      <img
        src={src}
        alt={alt}
        onLoad={() => setLoaded(true)}
        style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
        {...props}
      />
    </div>
  );
}

LQIP 先显示一张模糊的低质量占位图,然后过渡到完整图片。这种方案需要先生成一张极低分辨率的缩略图(可以在构建时用 Sharp 生成)。模糊效果使用 filter: blur() 实现,transform: scale(1.1) 是为了在模糊时避免边缘露出白色边框。


实战练习

练习 1:图片格式迁移

将项目中所有 JPEG/PNG 图片转换为 WebP/AVIF 格式,使用 <picture> 元素实现格式协商。使用 Lighthouse 对比迁移前后的性能指标变化,特别关注 LCP 和总阻塞时间。

练习 2:响应式图片系统

搭建一个自动化的图片处理流水线,输入原图后自动生成多尺寸、多格式的响应式图片集。使用 Sharp 或构建插件实现,配置合理的尺寸阶梯(如 400w、800w、1200w、1600w)和质量参数。

练习 3:LCP 图片优化

优化首屏 Hero 图片的加载:对图片进行格式优化、添加预加载标签、配置 fetchpriority 属性。使用 Lighthouse 和 web-vitals 测量优化前后的 LCP 变化,确保 LCP 降低至少 30%。


延展阅读


关键术语

术语 解释
WebP Google 开发的现代图片格式,支持有损和无损压缩,比 JPEG 小 25-35%
AVIF 基于 AV1 视频编码的图片格式,压缩率极高,但编码速度较慢
LQIP Low Quality Image Placeholder,低质量图片占位符技术
srcset HTML 属性,指定不同分辨率的图片候选
Art Direction 艺术指导,根据视口尺寸显示不同裁剪/构图图片的技术
fetchpriority HTML 属性,指定资源加载优先级
CDN Content Delivery Network,内容分发网络
Sharp Node.js 高性能图片处理库