做前端这些年,我见过太多把 <img> 和 <picture> 标签写混的情况。有人把所有图片都塞进 <picture> 里,有人连 srcset 都没用过,还有人因为图片格式不兼容在兼容性测试时被当场打脸。其实这俩标签并没有谁更高级,它们解决的是完全不同的两类问题:<img> 是基础,<picture> 是增强。这篇我会从使用场景出发,把它们的原理、写法、坑位一次说清楚,尤其适合正在做响应式页面、图片加载优化,或者被 WebP 兼容性折磨过的前端开发。看完之后,你至少能直接回答三个问题:什么时候用 <img> 就够了,什么时候必须上 <picture>,以及图片加载失败到底该怎么排查。
1. 先搞清楚 <img> 标签的本质与核心属性
1.1 替换元素、加载机制和宽高问题
很多新手会把 <img> 理解成“一个放图片的容器”,其实它是个典型的替换元素。替换元素的意思是:标签本身的内容不是由文本子节点决定的,而是被外部资源替换掉。所以 <img> 里写 <span> 没用,放文字也不会显示,浏览器只认 src 指向的那张图。
因为替换元素默认是 display: inline,它会被行内排版规则影响,比如基线对齐会在图片底部留出几个像素的空隙。这也是为什么很多项目会全局设置 img { display: block; }。但这只是视觉上的问题,更关键的是加载机制。
浏览器加载图片是异步的,但在图片还没加载完成时,如果标签没有显式宽高,布局就不知道要预留多少空间。等图片加载完,页面会被往下顶一下,这就是用户能感知到的“抖动”,也是性能指标里的 CLS 问题。解决办法很简单:给 img 写上 width 和 height,单位可以是像素,不需要 CSS。比如:
html复制<img src="photo.jpg" width="800" height="600" alt="风景">
这样浏览器会在图片加载前就按 800:600 的比例占位。注意,CSS 的 max-width: 100% 依然可以配合使用,只是比例先被定死了,不会抖。这是最容易被忽略,但回报率极高的小习惯。
1.2 src、alt 和其他高频属性
src 和 alt 是 <img> 的两个必备属性。src 不用多说,alt 却经常被写错。它不是“图片描述”的装饰品,而是图片加载失败、屏幕阅读器访问、SEO 爬虫理解图片内容时的唯一文本依据。
我把日常开发里真正用得上的属性整理成一张表,大家只需要记住这几个:
| 属性 | 作用 | 使用建议 |
|---|---|---|
src |
图片地址 | 必填,相对路径或绝对路径都可以 |
alt |
替代文本 | 必填,装饰性图片写空字符串 alt="" |
width / height |
显式尺寸 | 建议设置,防止 CLS |
loading |
加载策略 | 首屏不设,非首屏用 loading="lazy" |
decoding |
解码策略 | 可设 decoding="async" 提升渲染性能 |
fetchpriority |
加载优先级 | 首屏重要图片用 fetchpriority="high" |
referrerpolicy |
请求 referrer | 防盗链场景常用 no-referrer |
crossorigin |
跨域属性 | canvas 场景或需要 CORS 时使用 |
loading="lazy" 我单独提醒一句:它适合数量多、不在首屏的图片,但不要给首屏 LCP 图片加。浏览器虽然会智能判断,但实际测试中,给首屏图加 lazy 在部分浏览器里会拖慢加载,甚至导致 LCP 指标变差。首屏图不加,或者用 fetchpriority="high" 主动提权。
1.3 图片加载失败时,alt 是最后的兜底
很多人在图片加载失败后只看到一个小裂图图标,其实浏览器的真实行为是:如果 alt 有内容,就会把 alt 文本显示在裂图旁边;如果 alt 为空,就只显示一个占位框。所以 alt 写得好不好,直接决定了用户体验的下限。
我见过一个比较典型的例子:商品列表里,后端返回的商品图临时挂了 CDN,结果页面所有位置都是“product image”这种英文 alt,用户完全不知道那里本该是什么。正确的做法是写“白色运动鞋-侧面视角”这种有具体描述的文字,而不是笼统的“商品图”。
另外,alt 不是越长越好。搜索引擎理解图片内容主要靠它,但这不是让你堆关键词。把图片里的核心信息用一句自然的话说清楚就够了,装饰性图片老老实实写 alt="",这能让屏幕阅读器跳过无意义的图片描述,可访问性反而更好。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 响应式图片三件套:srcset、sizes 与 picture 的分工
2.1 srcset 与 sizes:让浏览器根据屏幕条件选图
srcset 和 sizes 是解决“同一张图在不同设备上显示不同大小”这个问题的最基础手段。它们不是让开发者手动判断设备,而是告诉浏览器有哪些候选图、每张图多宽,以及在当前布局下图片最终渲染宽度可能是多少,然后让浏览器自己选。
先看一个标准写法:
html复制<img src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="示例图片">
这里 400w、800w、1200w 是图片的“内在宽度描述符”,不是像素密度。浏览器会结合 sizes 里给出的布局宽度和当前设备的 DPR(设备像素比)来计算该加载哪张。比如当前视口宽度是 500px,sizes 里匹配到 100vw,那么图片渲染宽度大约是 500px,如果用户屏幕 DPR 是 2,浏览器就会优先选择宽度接近 1000px 的图,也就是 photo-1200.jpg。
srcset 还有一种写法是 1x、2x,只描述像素密度,不描述宽度:
html复制<img src="photo.jpg" srcset="photo@2x.jpg 2x" alt="示例图片">
这种写法适合图片显示宽度固定不变的场景,比如头像。但响应式布局里,推荐用 w 描述符加 sizes,更灵活。
2.2 picture 的设计目标:把“选图逻辑”从浏览器手里拿回来
srcset 让浏览器去选,但有些场景必须由开发者强制指定选哪张,这就是 <picture> 存在的意义。
<picture> 是一个容器,里面可以放多个 <source> 标签和一个 <img> 标签。浏览器会按顺序检查 <source> 的条件,第一个匹配的就加载;如果都不匹配,就使用最后的 <img> 作为兜底。这个设计很像路由:source 是路由规则,img 是默认路由。
看一个最简单的例子:
html复制<picture>
<source media="(min-width: 768px)" srcset="wide.jpg">
<source media="(min-width: 320px)" srcset="mobile-crop.jpg">
<img src="fallback.jpg" alt="风景图">
</picture>
当视口宽度大于等于 768px 时,加载 wide.jpg;在 320px 到 767px 之间,加载 mobile-crop.jpg;小于 320px 或浏览器不支持 source 时,加载 fallback.jpg。注意,img 必须写在最后,否则前面的 source 会失效。
source 还支持 type 属性,用来做图片格式回退。浏览器会跳过不能识别的 MIME 类型,选择第一个能认出的格式。这是 <picture> 最核心的使用场景之一。
2.3 两者如何配合使用
srcset 和 <picture> 不是互斥的,实际上它们经常配合。<source> 标签本身也支持 srcset,所以在 picture 里也可以结合分辨率切换和格式回退。
一句话总结分工:如果只是同一张图不同尺寸,用 <img> 配合 srcset 和 sizes;如果需要不同裁剪方向,或者需要在不同浏览器里用不同格式,用 <picture> 配合 <source> 的 media 和 type。下面这张表可以帮你快速决策:
| 需求 | 推荐方案 |
|---|---|
| 同一张图,适配不同屏幕宽度 | <img> + srcset + sizes |
| 同一张图,适配不同 DPR | <img> + srcset 的 1x/2x |
| 不同屏幕显示不同裁切效果 | <picture> + <source media> |
| 优先加载 WebP/AVIF,旧浏览器回退 | <picture> + <source type> |
| 既要做格式回退,又要分辨率切换 | <picture> + <source srcset> |
3. 具体使用场景拆解:img 还是 picture?
3.1 适合 img 的场景:普通内容图、头像、用户上传图
如果你只是要展示一张文章配图、用户头像、商品主图,并且所有设备上显示的内容范围是一样的,那就老老实实写 <img>。
比如博客封面图:手机上和电脑上看到的都是完整的图,只是显示尺寸不同。这种场景只需要准备两三张不同宽度的图片,用 srcset 和 sizes 让浏览器自己选。加上 width、height 占位,配合 loading="lazy"(非首屏),性能和体验都能兼顾。
用户上传图也是一样。头像裁剪通常固定比例,用 <img srcset="avatar-100.jpg 1x, avatar-200.jpg 2x"> 这种写法就够了,完全没有必要上 picture。给头像加 media 查询纯粹是过度设计,反而让代码更复杂。
3.2 适合 picture 的场景:艺术指导、格式回退
“艺术指导”这个翻译比较拗口,实际含义是:同一张内容图,在不同屏幕尺寸下需要展示不同的构图。
举个例子:一张旅游网站的头图,在桌面端可以显示完整的海岸线全景,但在手机窄屏上,如果缩小整张图,沙滩和人物就会变得很小,看不清重点。这时候你需要单独裁一张竖版或局部放大的图,专门给手机用户看。
- 桌面端加载
coast-wide.jpg - 手机端加载
coast-mobile.jpg
用 img 的 srcset 做不到这一点,因为 srcset 只是缩放整张图,不会切换裁剪区域。只能靠 picture 的 media 条件指定。
格式回退就更常用了。现在 WebP 在 Chrome、Edge、Firefox 都有很好的支持,AVIF 也逐渐普及,但总有些旧版 Safari 或某些国产浏览器不支持。你可以用 type 让现代浏览器优先加载体积更小的 WebP/AVIF,老浏览器自动降级到 JPEG/PNG:
html复制<picture>
<source type="image/avif" srcset="photo.avif">
<source type="image/webp" srcset="photo.webp">
<img src="photo.jpg" alt="示例图片">
</picture>
实际项目中,这种写法对流量优化非常明显。我遇到过一个图片占页面总资源 60% 以上的活动页,改成这种结构后,图片体积平均减少 40%,虽然代码多写了几行,但换来的性能提升是立竿见影的。
3.3 既不用 img 也不用 picture:CSS 背景图的边界
除了这两个标签,前端还有第三种展示图片的方式:CSS 背景图。很多人会忽略它,但它在某些场景下比 img 更合适。
CSS 背景图适合装饰性图片,比如渐变背景、纹理、图标、需要 background-size: cover 裁剪的背景块。这类图片通常不需要 SEO,也不要求屏幕阅读器识别,用 CSS 还能方便做 hover 切换、定位、平铺。
但是,如果图片是内容的一部分,比如产品图、新闻图、步骤截图,或者用户需要右键保存、需要被搜索引擎索引,那必须用 img 或 picture。不要因为“样式好写”就把内容图塞进 CSS 背景图,后面做 SEO 和无障碍时会非常痛苦。
4. 实操:从零搭建一套图片优化方案
4.1 第一步:生成多尺寸、多格式图片
方案落地前,你需要准备好不同尺寸和格式的图片文件。这里不建议手动用 PS 一张张导出,可以用工具批量处理。我在本地项目里经常用 ImageMagick 或 sharp 来做。
下面是一个最简单的 ImageMagick 命令,把原图 source.jpg 转成三张不同宽度的 WebP 和一张回退用的 JPEG:
bash复制convert source.jpg -resize 400x -quality 80 photo-400.jpg
convert source.jpg -resize 800x -quality 80 photo-800.jpg
convert source.jpg -resize 1200x -quality 80 photo-1200.jpg
convert source.jpg -resize 400x -quality 80 photo-400.webp
convert source.jpg -resize 800x -quality 80 photo-800.webp
convert source.jpg -resize 1200x -quality 80 photo-1200.webp
如果项目里有构建工具,也可以直接用 sharp 写一个 Node 脚本:读入一个目录下的所有 JPG/PNG,自动输出不同宽度的 WebP/AVIF。重点是:别在业务代码里做图片处理,提前打包压缩好,部署静态文件最省心。
4.2 第二步:完整代码示例,一个博客头图的两种写法
假设博客头图,桌面端显示完整横幅,移动端需要显示居中裁切,并且现代浏览器优先使用 WebP。完整的写法如下:
html复制<picture>
<source
media="(min-width: 768px)"
srcset="banner-1200.webp 1200w, banner-800.webp 800w"
type="image/webp"
sizes="100vw">
<source
media="(min-width: 768px)"
srcset="banner-1200.jpg 1200w, banner-800.jpg 800w"
type="image/jpeg"
sizes="100vw">
<source
srcset="banner-mobile.webp"
type="image/webp">
<img
src="banner-mobile.jpg"
alt="技术博客封面图"
width="1200"
height="600"
fetchpriority="high">
</picture>
这段代码做了三件事:
- 桌面端加载宽度合适的 WebP,如果浏览器不支持 WebP,回退到 JPEG。
- 移动端统一加载
banner-mobile.webp,兼容性差就加载banner-mobile.jpg。 - 最底层的
img负责兜底,并设置宽高和加载优先级。
注意,source 本身也可以写 sizes,上面例子里的 sizes="100vw" 放在桌面端的两个 source 里,意思是这张图始终占满视口宽度。移动端的 source 没有写 media,所以没有条件限制,作为最后的 <source>,会被 type 判断后再使用。
4.3 在 Vue 3 和 React 中动态使用时的注意点
在 Vue 3 或 React 里,图片地址经常会根据组件 props 变化。这时候很容易踩一个坑:把 srcset 用字符串拼接,结果浏览器加载不到图片。
Vue 3 推荐用 v-bind 绑定完整地址,尤其是图片路径由构建工具处理时。如果你把图片放在 src/assets 里,需要先导入再传入。例如:
vue复制<script setup>
import bannerWebp from '@/assets/banner.webp'
import bannerJpg from '@/assets/banner.jpg'
const props = defineProps(['src'])
</script>
<template>
<picture>
<source :srcset="bannerWebp" type="image/webp">
<img :src="bannerJpg" alt="封面图" width="1200" height="600">
</picture>
</template>
这里有个细节:在 <template> 里,srcset 和 src 都必须用 v-bind,也就是要写 :srcset、:src,不能直接写字符串。否则构建工具不会帮你处理资源路径,图片可能 404。
React 的话也差不多,JSX 里直接用变量赋值就行,但要注意 source 标签也需要 srcSet 驼峰写法,否则 React 会警告。比如 <source srcSet={bannerWebp} type="image/webp" />。
还需要提醒一点:如果你用了 CSS 模块或 scoped 样式,去掉对 picture 或 source 标签的怪异样式设置,因为这些不是普通元素,scoped 样式在某些浏览器下可能不会作用到 source 的渲染结果上。图片本身的样式,应该写在 img 上,而不是 picture 上。
5. 常见问题与排查技巧实录
5.1 img 图片加载失败:从路径到防盗链的排查清单
图片加载失败是前端最常遇到的问题,没有之一。很多人在控制台看到 404 就开始怀疑后端,但实际原因往往更基础。我整理了一张排查清单:
| 表现 | 可能原因 | 排查方法 |
|---|---|---|
| 图片直接裂开,控制台 404 | 路径写错、大小写不对 | 复制地址在浏览器打开,确认路径 |
| 本地正常,线上 404 | 打包后 publicPath 配置不对 | 检查构建配置或部署基础路径 |
| 图片 403 | 防盗链或 Referrer 策略 | 设置 referrerpolicy="no-referrer" |
| 图片一直不加载,控制台无报错 | 懒加载未触发或内存溢出 | 检查是否添加了 loading="lazy",或换成普通 img 测试 |
| 跨域图片画到 Canvas 报错 | CORS 限制 | 给 img 加 crossorigin="anonymous" |
最容易被忽略的是防盗链。很多图床和 CDN 默认禁止外部域名引用,本地打开正常,部署上线后全挂。这种情况可以先在浏览器 DevTools 的 Network 面板看响应状态码,如果是 403,优先在 img 上加上 referrerpolicy="no-referrer",如果还不行,就只能走服务端代理转发。
5.2 picture 标签不生效的五个原因
picture 不生效不像 img 失败那么直观,因为图片能显示,只是显示的不是你想要的那张。最常见的原因有五个:
<source>写在了<img>后面。浏览器只会把<img>前面的<source>当作候选,所以顺序不能反。<img>上没有写src。picture不是代替img,它只是给img加候选资源,img必须保留。media条件写反了。media="(max-width: 600px)"是小于等于 600px 才匹配,很多人会把它理解成大于等于,结果断点完全错位。type值写错。type="image/webp"是 MIME 类型,不是文件后缀。如果你写type="webp",浏览器会直接跳过。- 在 JS 里动态生成
source,但没有重新触发渲染。picture的内容变化不如img的src敏感,如果有动态切换需求,推荐直接更新img的src,或者用key强制重新渲染。
排查时,最简单的办法是在 DevTools 里查看当前渲染的 img 元素的 currentSrc 属性,它能告诉你浏览器到底选用了哪张图。如果 currentSrc 和你预期不符,再去检查 media 和 type。
5.3 CLS、LCP、懒加载与标签属性怎么配合
图片对性能指标的影响很大,尤其在移动端。CLS 的根源是图片没有预留空间,所以我前面强调一定要写 width 和 height。LCP 则是页面首屏最大内容元素的加载时间,如果你的 LCP 元素是图片,那它不应该被 loading="lazy"。
这里有个常见的误区:给所有图片统一加 loading="lazy",觉得这样能加速页面。实际上,首屏图片加 lazy 后,浏览器可能会延迟加载它,导致 LCP 时间变长。正确策略是:首屏关键图片不设置 loading,或者设置 fetchpriority="high";下方非首屏图片再加 loading="lazy"。
decoding="async" 也是一个性价比很高的属性。它让浏览器可以异步解码图片,不阻塞DOM渲染。对于大图,这个属性在某些浏览器上能明显减少卡顿。但注意,设置了 decoding="async" 和 loading="lazy" 一起用时,不要依赖图片的 onload 做强制同步逻辑,因为异步解码后事件触发时机可能晚于预期。
5.4 可访问性与 SEO:标签写不好真的会掉排名
图片的可访问性不只是给残障用户用的,它直接影响 SEO。搜索引擎爬虫看不懂图片内容,主要靠 alt、图片文件名、页面上下文来判断。一个写错了 alt 的图片,和一个没有 alt 的图片相比,后者至少不会被误解,前者可能把页面主题带偏。
alt 的写作原则可以用一句话概括:描述这张图片在上下文中“传递了什么信息”,而不是“图片里有什么”。比如文章里放了一张代码报错截图,alt 写成“控制台报错:Cannot read property 'map' of undefined” 比 “代码截图” 更能帮助理解上下文。
还有一个容易被忽略的点:picture 里的 source 不需要写 alt,alt 始终在 img 上。这样就算浏览器识别不了 source 里的格式,最终显示 img 的替代文本,SEO 也能正确抓取。
我在实际项目里踩过不少坑之后,现在给自己定了一条选型原则:内容图优先用 <img> 加 srcset,只有遇到格式兼容和定向裁切需求时才上 <picture>,装饰图直接用 CSS 背景图。这样代码结构最干净,也最容易维护。如果你现在正在改一个图片加载失败的页面,建议先从 DevTools 的 Network 面板看状态码,再回过来检查 srcset 和 picture 的写法,大部分问题都能在这一步暴露出来。
