响应式图片的工程化实践:srcset与picture

28 阅读7分钟

前言

在移动优先和多设备并存的今天,一张图片要在不同尺寸、不同分辨率的屏幕上都能完美展示,是一项极具挑战性的任务。一个简单的<img src="photo.jpg">会导致:

  • Retina屏上图片模糊:1x图在2x屏上被拉伸
  • 移动端加载超大图片:下载了PC端的大图,浪费流量
  • 横竖屏切换时构图不当:竖屏显示的图片被强行裁剪

响应式图片技术正是为解决这些问题而生。本文将深入探讨srcsetpicture的核心原理,并通过Vue组件封装和Vite插件实现,建立一套工程化的响应式图片解决方案。

为什么需要响应式图片?

传统方式:一张图片走天下

<img src="photo.jpg" alt="风景">

传统方式的问题

  • iPhone SE (小屏) → 下载 5MB 的大图 → 浪费
  • iPad (中屏) → 下载 5MB 的大图 → 还行
  • MacBook (大屏) → 下载 5MB 的大图 → 刚好
  • Retina 屏幕 → 下载 5MB 的普通图 → 模糊

设备像素比(DPR)

什么是设备像素比

**设备像素比(Device Pixel Ratio)**是物理像素与逻辑像素的比值:

// 获取当前设备的像素比
const dpr = window.devicePixelRatio || 1;
console.log(dpr); // 普通屏: 1, Retina屏: 2, 高端屏: 3或更高

设备像素比的典型值范围

  • 普通屏幕:DPR = 1
  • Retina屏幕:DPR = 2 / 3
  • 4K屏幕:DPR = 3+

为什么需要关注DPR?

当我们在CSS中设置width: 100px时,在 DPR=2 的屏幕上,实际需要 200 个物理像素来渲染。如果只提供 100px 的图片,就会被拉伸模糊。

三个核心问题

问题1:屏幕大小不同

  • 手机小屏:不需要大图
  • 平板中屏:需要中等图
  • 电脑大屏:需要高清图

问题2:像素密度不同

  • 普通屏:1x 图就够了
  • Retina 屏:需要 2x 图
  • 高端屏:需要 3x 图

问题3:屏幕方向不同

  • 横屏:适合宽幅风景
  • 竖屏:适合高耸人像

srcset - 让浏览器自己选

x描述符(根据像素密度)

告诉浏览器:我有 1x、2x、3x 三个版本:

<img 
  src="photo-1x.jpg"
  srcset="
    photo-1x.jpg 1x,
    photo-2x.jpg 2x,
    photo-3x.jpg 3x
  "
  alt="风景"
>

浏览器在解析时,就会自动选择:

  • iPhone 14 Pro (DPR=3) → 加载 photo-3x.jpg
  • iPhone SE (DPR=2) → 加载 photo-2x.jpg
  • 普通电脑 (DPR=1) → 加载 photo-1x.jpg

w描述符(根据屏幕宽度)

<img 
  src="photo-400w.jpg"
  srcset="
    photo-400w.jpg 400w,
    photo-800w.jpg 800w,
    photo-1200w.jpg 1200w
  "
  sizes="
    (max-width: 600px) 100vw,
    (max-width: 1200px) 50vw,
    800px
  "
  alt="风景"
>

sizes是怎么计算的?

sizes属性告诉浏览器在不同视口宽度下,图像的实际显示宽度,如:

sizes="
  (max-width: 600px) 100vw,   /* 小屏幕:图片占满视口宽度 */
  (max-width: 1200px) 50vw,   /* 中屏幕:图片占视口一半 */
  800px                        /* 大屏幕:图片固定800px */
"

其计算逻辑如下:

  1. 浏览器检查 sizes:sizes: "(max-width: 600px) 100vw, ..."
  2. 匹配条件 (max-width: 600px) 满足:图片宽度 = 100vw = 375px
  3. 考虑 DPR (iPhone SE DPR=2):实际需要 = 375px × 2 = 750px 的图片
  4. 从 srcset 中选择最接近的:400w 太小,1200w 太大 → 选择 800w

picture - 让开发者控制

什么时候需要 picture?

srcset 可以解决图片大小问题,但不能解决构图问题。比如:横屏时,我们需要展示完整的风景;竖屏时,我们需要展示裁剪后的人像,此时 picture 就派上用场了!

picture 的元素的结构

<picture>
  <!-- 针对宽屏的横图 -->
  <source 
    media="(min-width: 1200px)" 
    srcset="hero-wide.jpg"
  >
  <!-- 针对平板的方图 -->
  <source 
    media="(min-width: 768px)" 
    srcset="hero-square.jpg"
  >
  <!-- 针对手机的竖图 -->
  <source 
    media="(max-width: 767px)" 
    srcset="hero-tall.jpg"
  >
  <!-- 降级方案 -->
  <img src="hero-fallback.jpg" alt="Hero image">
</picture>

浏览器会按顺序检查 <source> 元素,选择第一个匹配的媒体条件。

不同格式降级

picture还可以根据浏览器支持的格式提供不同的降级方案:

<picture>
  <!-- 优先使用AVIF(压缩率最高) -->
  <source srcset="image.avif" type="image/avif">
  <!-- 其次使用WebP(广泛支持) -->
  <source srcset="image.webp" type="image/webp">
  <!-- 降级到JPEG(兜底) -->
  <img src="image.jpg" alt="Fallback">
</picture>

srcset vs picture 选择策略

场景推荐方案原因
不同分辨率(2x/3x屏)srcset + x描述符简单直接,浏览器自动选择
不同视口宽度srcset + w描述符 + sizes精确控制加载尺寸
不同构图/裁剪picture + media艺术指导需求
不同格式降级picture + type渐进增强,兼容老旧浏览器

Vue 组件封装:<ResponsiveImage>的设计与实现

组件设计

<!-- ResponsiveImage.vue -->
<template>
  <picture v-if="usePicture">
    <!-- 为每种格式生成 source -->
    <source
      v-for="source in pictureSources"
      :key="source.type"
      :type="source.type"
      :srcset="source.srcset"
      :media="source.media"
    >
    <!-- 兜底图 -->
    <img :src="fallbackSrc" :alt="alt" loading="lazy">
  </picture>
  
  <img
    v-else
    :src="src"
    :srcset="srcsetString"
    :sizes="sizes"
    :alt="alt"
    loading="lazy"
  >
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
  // 基础配置
  src: String,           // 原图地址
  alt: String,           // 替代文本
  
  // 响应式配置
  widths: {
    type: Array,
    default: () => [400, 800, 1200]
  },
  formats: {
    type: Array,
    default: () => ['webp', 'avif']
  },
  sizes: {
    type: String,
    default: '100vw'
  },
  
  // 艺术指导
  mobile: String,        // 手机版图片
  tablet: String,        // 平板版图片
  desktop: String        // 桌面版图片
})

// 判断是否使用 picture 模式
const usePicture = computed(() => {
  return props.mobile || props.tablet || props.desktop
})

// 生成 srcset 字符串
const generateSrcset = (basePath, widths, format) => {
  return widths
    .map(w => `${basePath}-${w}w.${format} ${w}w`)
    .join(', ')
}

// picture 模式的 sources
const pictureSources = computed(() => {
  const sources = []
  
  // 为每种格式生成 source
  props.formats.forEach(format => {
    // 桌面版
    if (props.desktop) {
      sources.push({
        media: '(min-width: 1200px)',
        srcset: generateSrcset(props.desktop, props.widths, format),
        type: `image/${format}`
      })
    }
    
    // 平板版
    if (props.tablet) {
      sources.push({
        media: '(min-width: 768px) and (max-width: 1199px)',
        srcset: generateSrcset(props.tablet, props.widths, format),
        type: `image/${format}`
      })
    }
    
    // 手机版
    if (props.mobile) {
      sources.push({
        media: '(max-width: 767px)',
        srcset: generateSrcset(props.mobile, props.widths, format),
        type: `image/${format}`
      })
    }
  })
  
  return sources
})

// 兜底图片
const fallbackSrc = computed(() => {
  return props.desktop || props.tablet || props.mobile || props.src
})

// 非 picture 模式的 srcset
const srcsetString = computed(() => {
  if (usePicture.value) return ''
  return generateSrcset(props.src, props.widths, 'jpg')
})
</script>

组件使用示例

<template>
  <!-- 方案1:普通响应式图片 -->
  <ResponsiveImage
    src="/images/photo.jpg"
    :widths="[400, 800, 1200]"
    sizes="(max-width: 600px) 100vw, 50vw"
    alt="风景"
  />
  
  <!-- 方案2:艺术指导(不同屏幕不同构图) -->
  <ResponsiveImage
    mobile="/images/hero-mobile.jpg"
    tablet="/images/hero-tablet.jpg"
    desktop="/images/hero-desktop.jpg"
    :widths="[400, 800, 1200]"
    alt="英雄图"
  />
</template>

自动生成多尺寸图片 - Vite 插件

为什么需要插件生成?

假如我们需要手动为每张图片生成:

  • photo-400w.jpg
  • photo-800w.jpg
  • photo-1200w.jpg
  • photo-400w.webp
  • photo-800w.webp
  • photo-1200w.webp
  • photo-400w.avif
  • photo-800w.avif
  • photo-1200w.avif

相当于一张图片就要配置 9 个文件;随着图片数量的增加,这将是一场噩梦!

插件原理与设计

  1. 识别项目中的图片导入
  2. 根据配置生成多种尺寸和格式
  3. 注入对应的 srcset 信息

Vite插件完整实现

/// vite-plugin-responsive-images.js
import sharp from 'sharp'
import { glob } from 'fast-glob'

export default function responsiveImagesPlugin(options) {
  const {
    widths = [400, 800, 1200],
    formats = ['webp', 'avif'],
    quality = 80
  } = options
  
  return {
    name: 'vite-plugin-responsive-images',
    
    async buildStart() {
      // 找到所有图片
      const files = await glob('src/assets/images/**/*.{jpg,jpeg,png}')
      
      console.log(`📸 找到 ${files.length} 张图片`)
      
      for (const file of files) {
        // 为每个尺寸和格式生成图片
        for (const width of widths) {
          for (const format of formats) {
            const outputPath = file
              .replace('src/assets', 'dist/assets')
              .replace(/\.(jpg|jpeg|png)$/, `-${width}w.${format}`)
            
            await sharp(file)
              .resize(width, null, { withoutEnlargement: true })
              .toFormat(format, { quality })
              .toFile(outputPath)
          }
        }
      }
      
      console.log('✅ 图片生成完成')
    }
  }
}

配置插件

// vite.config.js
import responsiveImages from './vite-plugin-responsive-images'

export default {
  plugins: [
    responsiveImages({
      widths: [400, 800, 1200, 1600],
      formats: ['webp', 'avif'],
      quality: 75
    })
  ]
}

性能对比:不同方案下的图片加载体积

测试数据对比

基于典型电商商品详情页的测试结果:

图片类型原始大小WebPAVIF节省空间
商品主图 (1200×1200)850KB320KB210KB62%-75%
商品缩略图 (400×400)120KB45KB28KB62%-77%
轮播大图 (1920×1080)1.2MB480KB320KB60%-73%

响应式方案加载体积对比

设备传统单图仅WebP响应式srcset响应式+WebP+AVIF
iPhone SE (375pt)下载1200w图 (850KB)下载1200w图 (320KB)下载400w图 (120KB)下载400w WebP (45KB)
iPad (768pt)下载1200w图 (850KB)下载1200w图 (320KB)下载800w图 (280KB)下载800w WebP (98KB)
MacBook Pro下载1200w图 (850KB)下载1200w图 (320KB)下载1200w图 (850KB)下载1200w WebP (320KB)
平均节省基准62%51%80%

加载性能指标提升

指标优化前优化后提升
LCP (最大内容绘制)3.2s1.4s56%
图片请求数12833%
总图片体积4.2MB1.1MB74%
移动端数据消耗4.2MB/次访问0.6MB/次访问86%

最佳实践清单

配置建议

图片尺寸断点:
├─ 400w:手机小屏
├─ 800w:手机大屏/平板
├─ 1200w:笔记本电脑
├─ 1600w:台式机
└─ 2000w:4K 屏幕

图片格式优先级:
├─ AVIF(最新,压缩率最高)
├─ WebP(广泛支持)
└─ JPEG/PNG(兜底)

sizes 设置:
├─ 手机:(max-width: 600px) 100vw
├─ 平板:(max-width: 1200px) 50vw
└─ 电脑:800px

实施策略选择矩阵

场景技术方案关键配置
普通内容图片srcset + sizes提供3-5种宽度,设置合理sizes
图标/Logosrcset + x描述符提供1x/2x/3x版本
不同构图需求picture + media针对断点设计不同裁剪
现代格式降级picture + typeAVIF → WebP → JPEG
用户上传内容动态生成 + CDN处理根据设备实时转换

实施清单

  • 所有图片提供 3-5 种尺寸
  • 生成 WebP 和 AVIF 格式
  • 使用 <picture> 实现格式降级
  • 设置正确的 sizes 属性
  • 关键图片设置 loading="eager"
  • 非关键图片设置 loading="lazy"
  • 使用 Vite 插件自动生成多尺寸

结业

用户可能不会注意到图片加载很快,但一定会注意到图片加载很慢。响应式图片优化,是对用户体验最深情的告白。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!