前言
在移动优先和多设备并存的今天,一张图片要在不同尺寸、不同分辨率的屏幕上都能完美展示,是一项极具挑战性的任务。一个简单的<img src="photo.jpg">会导致:
- Retina屏上图片模糊:1x图在2x屏上被拉伸
- 移动端加载超大图片:下载了PC端的大图,浪费流量
- 横竖屏切换时构图不当:竖屏显示的图片被强行裁剪
响应式图片技术正是为解决这些问题而生。本文将深入探讨srcset与picture的核心原理,并通过Vue组件封装和Vite插件实现,建立一套工程化的响应式图片解决方案。
为什么需要响应式图片?
传统方式:一张图片走天下
html
<img src="photo.jpg" alt="风景">
传统方式的问题
- iPhone SE (小屏) → 下载 5MB 的大图 → 浪费
- iPad (中屏) → 下载 5MB 的大图 → 还行
- MacBook (大屏) → 下载 5MB 的大图 → 刚好
- Retina 屏幕 → 下载 5MB 的普通图 → 模糊
设备像素比(DPR)
什么是设备像素比
**设备像素比(Device Pixel Ratio)**是物理像素与逻辑像素的比值:
javascript
// 获取当前设备的像素比
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 三个版本:
html
<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描述符(根据屏幕宽度)
html
<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属性告诉浏览器在不同视口宽度下,图像的实际显示宽度,如:
javascript
sizes="
(max-width: 600px) 100vw, /* 小屏幕:图片占满视口宽度 */
(max-width: 1200px) 50vw, /* 中屏幕:图片占视口一半 */
800px /* 大屏幕:图片固定800px */
"
其计算逻辑如下:
- 浏览器检查 sizes:
sizes: "(max-width: 600px) 100vw, ..." - 匹配条件 (max-width: 600px) 满足:
图片宽度 = 100vw = 375px - 考虑 DPR (iPhone SE DPR=2):
实际需要 = 375px × 2 = 750px 的图片 - 从 srcset 中选择最接近的:
400w 太小,1200w 太大 → 选择 800w
picture - 让开发者控制
什么时候需要 picture?
srcset 可以解决图片大小问题,但不能解决构图问题。比如:横屏时,我们需要展示完整的风景;竖屏时,我们需要展示裁剪后的人像,此时 picture 就派上用场了!
picture 的元素的结构
html
<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还可以根据浏览器支持的格式提供不同的降级方案:
html
<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>的设计与实现
组件设计
html
<!-- 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>
组件使用示例
html
<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 个文件;随着图片数量的增加,这将是一场噩梦!
插件原理与设计
- 识别项目中的图片导入
- 根据配置生成多种尺寸和格式
- 注入对应的 srcset 信息
Vite插件完整实现
typescript
/// 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('✅ 图片生成完成')
}
}
}
配置插件
typescript
// vite.config.js
import responsiveImages from './vite-plugin-responsive-images'
export default {
plugins: [
responsiveImages({
widths: [400, 800, 1200, 1600],
formats: ['webp', 'avif'],
quality: 75
})
]
}
性能对比:不同方案下的图片加载体积
测试数据对比
基于典型电商商品详情页的测试结果:
| 图片类型 | 原始大小 | WebP | AVIF | 节省空间 |
|---|---|---|---|---|
| 商品主图 (1200×1200) | 850KB | 320KB | 210KB | 62%-75% |
| 商品缩略图 (400×400) | 120KB | 45KB | 28KB | 62%-77% |
| 轮播大图 (1920×1080) | 1.2MB | 480KB | 320KB | 60%-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.2s | 1.4s | 56% |
| 图片请求数 | 12 | 8 | 33% |
| 总图片体积 | 4.2MB | 1.1MB | 74% |
| 移动端数据消耗 | 4.2MB/次访问 | 0.6MB/次访问 | 86% |
最佳实践清单
配置建议
text
图片尺寸断点:
├─ 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 |
| 图标/Logo | srcset + x描述符 |
提供1x/2x/3x版本 |
| 不同构图需求 | picture + media |
针对断点设计不同裁剪 |
| 现代格式降级 | picture + type |
AVIF → WebP → JPEG |
| 用户上传内容 | 动态生成 + CDN处理 | 根据设备实时转换 |
实施清单
- 所有图片提供 3-5 种尺寸
- 生成 WebP 和 AVIF 格式
- 使用
<picture>实现格式降级 - 设置正确的 sizes 属性
- 关键图片设置 loading="eager"
- 非关键图片设置 loading="lazy"
- 使用 Vite 插件自动生成多尺寸
结业
用户可能不会注意到图片加载很快,但一定会注意到图片加载很慢。响应式图片优化,是对用户体验最深情的告白。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!