前置知识: HTML5

图像与响应式图片

18 min中级

img、srcset、sizes、picture元素

前置知识

建议先阅读以下内容再进入本文:

0基础速通:读第 0 节直觉、第 1-3 章核心概念速览与第 4 章代码示例即可;第 6 章深入理解(选读)供进阶。

1. 历史动机与发展脉络

1.1 早期图像时代(1993—2000)

Tim Berners-Lee 在 1991 年的 WorldWideWeb 浏览器中仅支持文本。1993 年 Marc Andreessen 在 NCSA Mosaic 中引入 <img> 标签,引发”图像爆炸”。

<!-- 1993 年 Mosaic 的原始 <img> 语法 -->
<img src="photo.gif" alt="A photo">

早期图像格式:

格式发布年压缩透明动画颜色深度
GIF1987LZW 无损1 bit支持8 bit (256 色)
JPEG1992DCT 有损不支持不支持24 bit
PNG1996DEFLATE 无损8 bit alpha不支持48 bit

1.2 移动互联网的挑战(2007—2013)

iPhone(2007)开启移动互联网时代。2010 年 retina 显示屏(DPR=2)发布,传统单图方案导致:

  1. 高清屏模糊:1x 图在 2x 屏上像素拉伸。
  2. 流量浪费:2x 图在 1x 屏下载冗余字节。
  3. 视口适配差:手机下载桌面大图。

早期解决方案:

<!-- 方案 A:JS 检测后切换 -->
<script>
  if (window.devicePixelRatio > 1) {
    document.querySelector('img').src = 'photo@2x.jpg';
  }
</script>

<!-- 方案 B:CSS 媒体查询 + background-image -->
<style>
  .hero { background-image: url(photo.jpg); }
  @media (-webkit-min-device-pixel-ratio: 2) {
    .hero { background-image: url(photo@2x.jpg); }
  }
</style>

这些方案的缺陷:JS 方案阻塞渲染、CSS 方案无法用于内容图片、HTTP 缓存易失效。

1.3 HTML5 响应式图片规范(2012—2016)

2012 年 W3C HTML Responsive Images Community Group 提出 srcset 与 <picture> 提案。2014 年 HTML5.1 草案正式纳入:

  • srcset 属性:候选图像列表,浏览器自动选择。
  • sizes 属性:声明图片渲染宽度,辅助选择。
  • <picture> 元素:基于媒体查询的显式切换。

2016 年 HTML5.1 成为 W3C 推荐标准,响应式图片 API 全面落地。

1.4 现代图像格式时代(2010—2024)

格式发布年编码器压缩率(vs JPEG)浏览器支持
WebP2010libwebp (VP8)-25%~ -35%97%+
HEIC2017HEVC-50%iOS/macOS only
AVIF2019libavif (AV1)-50%~-70%92%+
JPEG XL2022libjxl-60%70%(需 feature flag)

1.5 演进时间线

timeline
    title 发展时间线
    1993: Mosaic 引入 <img>
    1995: HTML 2.0 (RFC 1866) 规范化 <img>
    1996: PNG 1.0 发布(W3C 推荐)
    2007: iPhone 发布,移动互联网时代开启
    2010: Retina 显示屏;WebP 发布
    2012: W3C 响应式图片提案(srcset / picture)
    2014: HTML5 标准化(不含响应式图片)
    2016: HTML5.1 纳入 srcset / sizes / <picture>
    2017: Chrome 56 支持 <img loading="lazy">(实验性)
    2019: AVIF 1.0 发布
    2020: Chrome 85 <img loading="lazy"> 正式可用
    2022: JPEG XL 1.0 发布
    2023: fetchpriority 属性进入 HTML Living Standard
    2024: AVIF 全球支持率 >92%;JPEG XL 仍在 Chrome flag 阶段

1.6 规范族谱

  • HTML 2.0(RFC 1866, 1995):<img> 基本语法。
  • HTML 4.01(W3C, 1999):增加 longdesc、usemap、ismap。
  • HTML5(W3C, 2014):增加 crossorigin、srcset(草案)。
  • HTML 5.1(W3C, 2016):正式纳入 srcset/sizes/<picture>。
  • HTML 5.2 / 5.3(W3C, 2017—2018):增加 loading、decoding、referrerpolicy。
  • WHATWG HTML Living Standard(持续更新):§4.8.3 “The img element”、§4.8.4.2 “The picture element” 为权威参考。

2. 形式化定义

2.1 WHATWG 规范定义

依据 WHATWG HTML Living Standard §4.8.3,<img> 元素的 Web IDL 定义:

[Exposed=Window]
interface HTMLPictureElement : HTMLElement {};

[Exposed=Window]
interface HTMLImageElement : HTMLElement {
  [CEReactions] attribute USVString src;
  [CEReactions] attribute USVString currentSrc;
  [CEReactions] attribute DOMString alt;
  [CEReactions] attribute DOMString srcset;
  [CEReactions] attribute DOMString sizes;
  [CEReactions] attribute DOMString? crossOrigin;
  [CEReactions] attribute DOMString useMap;
  [CEReactions] attribute boolean isMap;
  [CEReactions] attribute unsigned long width;
  [CEReactions] attribute unsigned long height;
  [CEReactions] attribute DOMString decoding;
  [CEReactions] attribute DOMString loading;
  [CEReactions] attribute DOMString fetchPriority;
  [CEReactions] attribute DOMString referrerPolicy;
  readonly attribute boolean complete;
  readonly attribute unsigned long naturalWidth;
  readonly attribute unsigned long naturalHeight;
  Promise<undefined> decode();
};

[Exposed=Window]
interface HTMLSourceElement : HTMLElement {
  [CEReactions] attribute USVString src;
  [CEReactions] attribute DOMString srcset;
  [CEReactions] attribute DOMString sizes;
  [CEReactions] attribute DOMString type;
  [CEReactions] attribute DOMString media;
  [CEReactions] attribute unsigned long width;
  [CEReactions] attribute unsigned long height;
};

2.2 srcset 文法

srcset = candidate ( "," candidate )*
candidate = URL [ descriptor ]
descriptor = width-descriptor | pixel-density-descriptor
width-descriptor = positive-integer "w"
pixel-density-descriptor = positive-floating-point "x"

约束:

  • 同一 srcset 中所有 candidate 必须使用相同类型的描述符(不能 w 与 x 混用)。
  • 默认描述符为 1x(即未显式指定时)。

2.3 sizes 文法

sizes = size-source ( "," size-source )*
size-source = [ media-condition ] length
length = CSS <length> 值(如 100vw, 50vw, 800px)

2.4 图像选择算法形式化

设浏览器视口宽度为 VV(CSS 像素),设备像素比为 DD,sizes 计算出的渲染宽度为 RR(CSS 像素)。

定义 3.4.1(候选集):srcset 解析后得到候选集 C={(ui,wi)}C = \{(u_i, w_i)\}(宽度描述符)或 C={(ui,di)}C = \{(u_i, d_i)\}(密度描述符)。

定义 3.4.2(密度描述符选择):选择使 ∣di−D∣|d_i - D| 最小的候选;若并列,选 di≥Dd_i \geq D 中最小者。

定义 3.4.3(宽度描述符选择):

targetWidth=R×D\text{targetWidth} = R \times D

选择使 ∣wi−targetWidth∣|w_i - \text{targetWidth}| 最小的候选;若并列,选 wi≥targetWidthw_i \geq \text{targetWidth} 中最小者。

示例:V=400pxV = 400\text{px}, D=2D = 2, sizes="(max-width: 600px) 100vw, 50vw"。则 R=400pxR = 400\text{px}(命中媒体条件),targetWidth=800px\text{targetWidth} = 800\text{px}。若 srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w",则选择 medium.jpg。

2.5 LCP(Largest Contentful Paint)形式化

设图像 II 的下载时间为 TdownloadT_{\text{download}},解码时间为 TdecodeT_{\text{decode}},渲染时间为 TrenderT_{\text{render}}。LCP 时刻:

LCP(I)=TrequestStart+Tdownload+Tdecode+Trender\text{LCP}(I) = T_{\text{requestStart}} + T_{\text{download}} + T_{\text{decode}} + T_{\text{render}}

图像字节数 BB 与下载时间近似成正比:

Tdownload≈BbandwidthT_{\text{download}} \approx \frac{B}{\text{bandwidth}}

故减小 BB(响应式图片)直接降低 LCP。

2.6 布局偏移(CLS)形式化

设图像加载前容器高度为 h0h_0,加载后为 h1h_1。布局偏移量:

Δh=h1−h0\Delta h = h_1 - h_0

若 Δh≠0\Delta h \neq 0,则贡献 CLS。修复:通过 <img width="W" height="H"> 显式声明比例,浏览器在加载前预留盒子:

aspect-ratio=WH\text{aspect-ratio} = \frac{W}{H}

CSS 自动按此比例计算高度,避免布局偏移。


3. 理论推导与原理解析

3.1 设备像素比(DPR)的物理意义

设物理像素数为 PphysicalP_{\text{physical}},CSS 像素数为 PcssP_{\text{css}}。

D=PphysicalPcssD = \frac{P_{\text{physical}}}{P_{\text{css}}}

示例:iPhone 12 物理分辨率 1170×2532,CSS 视口 390×844,则 D=3D = 3。

3.2 图像字节量与质量关系

JPEG 质量参数 q∈[1,100]q \in [1, 100],字节数 B(q)B(q) 与峰值信噪比(PSNR)近似:

B(q)∝q,PSNR(q)∝log⁡qB(q) \propto q, \quad \text{PSNR}(q) \propto \log q

实证数据(800×600 风景图):

质量 qqJPEG 字节WebP 字节AVIF 字节PSNR
90156 KB110 KB78 KB42 dB
8098 KB68 KB48 KB38 dB
7072 KB50 KB35 KB35 dB
5048 KB33 KB22 KB31 dB

3.3 响应式图片的带宽节省

设传统方案下载固定 Bfixed=1MBB_{\text{fixed}} = 1\text{MB} 图片。响应式方案按视口选择 B(v)B(v):

B(v)=α⋅v2,α=BmaxVmax2B(v) = \alpha \cdot v^2, \quad \alpha = \frac{B_{\text{max}}}{V_{\text{max}}^2}

设 Vmax=1920pxV_{\text{max}} = 1920\text{px},Bmax=1MBB_{\text{max}} = 1\text{MB}。则 α≈2.7×10−4 KB/px2\alpha \approx 2.7 \times 10^{-4}\text{ KB/px}^2。

带宽节省率(视口 V=400pxV = 400\text{px}):

η=1−B(400)Bfixed=1−2.7×10−4×40021024≈95.8%\eta = 1 - \frac{B(400)}{B_{\text{fixed}}} = 1 - \frac{2.7 \times 10^{-4} \times 400^2}{1024} \approx 95.8\%

3.4 DCT 与压缩原理

JPEG 使用 8×88 \times 8 块的二维 DCT:

F(u,v)=14C(u)C(v)∑x=07∑y=07f(x,y)cos⁡((2x+1)uπ16)cos⁡((2y+1)vπ16)F(u, v) = \frac{1}{4} C(u) C(v) \sum_{x=0}^{7} \sum_{y=0}^{7} f(x, y) \cos\left(\frac{(2x+1)u\pi}{16}\right) \cos\left(\frac{(2y+1)v\pi}{16}\right)

其中 C(u)=12C(u) = \frac{1}{\sqrt{2}} if u=0u=0 else 11。

低频系数(左上角)集中能量,高频系数(右下角)可被量化丢弃。WebP(VP8)使用 4×44 \times 4 块 + 帧内预测,AVIF(AV1)使用 4×44 \times 4 到 64×6464 \times 64 自适应块 + 多参考帧预测,压缩率更高。

3.5 懒加载的视口检测算法

浏览器原生懒加载(loading="lazy")使用 IntersectionObserver 替代(用户不可见)。设图片距视口底部距离为 dd,懒加载触发距离 dthresholdd_{\text{threshold}}(默认 1250px on 4G,3000px on 3G)。

load={trueif d≤dthresholdfalseotherwise\text{load} = \begin{cases} \text{true} & \text{if } d \leq d_{\text{threshold}} \\ \text{false} & \text{otherwise} \end{cases}

3.6 解码异步化

decoding="async" 将解码移出主线程,避免阻塞首次渲染。

TmainThread=Tdecode, sync→0(async)T_{\text{mainThread}} = T_{\text{decode, sync}} \to 0 \quad (\text{async})

代价:图片显示延迟约 1 帧(16ms)。


4. 代码示例

4.1 完整 HTML5 文档结构

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>响应式图片示例</title>
    <style>
      img { max-width: 100%; height: auto; }
      .hero { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
    </style>
  </head>
  <body>
    <!-- 1. 基础图片 -->
    <img src="photo.jpg" alt="风景照" width="800" height="600" />

    <!-- 2. 响应式图片(srcset + sizes) -->
    <img
      src="photo-800.jpg"
      srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w, photo-1600.jpg 1600w"
      sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
      alt="响应式风景照"
      width="1600"
      height="1200"
      loading="lazy"
      decoding="async"
      fetchpriority="low"
    />

    <!-- 3. 艺术指导(picture + media) -->
    <picture>
      <source media="(max-width: 600px)" srcset="photo-mobile.jpg" />
      <source media="(max-width: 1200px)" srcset="photo-tablet.jpg" />
      <source media="(min-width: 1201px)" srcset="photo-desktop.jpg" />
      <img src="photo-desktop.jpg" alt="艺术指导示例" width="1600" height="900" />
    </picture>

    <!-- 4. 格式协商(picture + type) -->
    <picture>
      <source srcset="photo.avif" type="image/avif" />
      <source srcset="photo.webp" type="image/webp" />
      <source srcset="photo.jp2" type="image/jp2" />
      <img src="photo.jpg" alt="格式协商示例" width="800" height="600" loading="lazy" />
    </picture>

    <!-- 5. LCP 图片(高优先级 + 预加载) -->
    <link rel="preload" as="image" href="hero.avif" type="image/aviffetchpriority="high" />
    <img src="hero.avif" alt="首屏主视觉" class="hero" fetchpriority="high" decoding="async" />

    <!-- 6. 内联 SVG(矢量图) -->
    <svg viewBox="0 0 100 100" width="100" height="100" role="img" aria-label="图标">
      <circle cx="50" cy="50" r="40" fill="#4CAF50" />
    </svg>
  </body>
</html>

4.2 srcset 完整示例(宽度描述符)

<img
  src="photo-800.jpg"
  srcset="
    photo-320.jpg   320w,
    photo-480.jpg   480w,
    photo-640.jpg   640w,
    photo-800.jpg   800w,
    photo-1024.jpg 1024w,
    photo-1280.jpg 1280w,
    photo-1600.jpg 1600w,
    photo-1920.jpg 1920w,
    photo-2560.jpg 2560w
  "
  sizes="
    (max-width: 320px) 280px,
    (max-width: 480px) 440px,
    (max-width: 768px) 720px,
    (max-width: 1024px) 480px,
    (max-width: 1280px) 600px,
    800px
  "
  alt="完整响应式示例"
  width="2560"
  height="1440"
  loading="lazy"
  decoding="async"
/>

4.3 像素密度描述符

<!-- 适用于图标、Logo 等固定尺寸场景 -->
<img
  src="logo.png"
  srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
  alt="Logo"
  width="200"
  height="50"
/>

4.4 艺术指导(Art Direction)

<!-- 桌面版显示完整合影,移动版聚焦人脸 -->
<picture>
  <source
    media="(max-width: 600px) and (orientation: portrait)"
    srcset="team-mobile-cropped.jpg"
    sizes="100vw"
  />
  <source
    media="(min-width: 601px)"
    srcset="team-desktop-full.jpg"
    sizes="(max-width: 1200px) 100vw, 1200px"
  />
  <img
    src="team-desktop-full.jpg"
    alt="团队合影"
    width="1600"
    height="900"
    loading="lazy"
  />
</picture>

4.5 现代格式协商

<picture>
  <source
    srcset="
      photo-400.avif 400w,
      photo-800.avif 800w,
      photo-1200.avif 1200w
    "
    sizes="(max-width: 600px) 100vw, 50vw"
    type="image/avif"
  />
  <source
    srcset="
      photo-400.webp 400w,
      photo-800.webp 800w,
      photo-1200.webp 1200w
    "
    sizes="(max-width: 600px) 100vw, 50vw"
    type="image/webp"
  />
  <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="现代格式协商"
    width="1200"
    height="800"
    loading="lazy"
    decoding="async"
  />
</picture>

4.6 生产级 React 组件

// ResponsiveImage.jsx
// 生产级响应式图片 React 组件

import { useState, useEffect } from 'react';

const BREAKPOINTS = {
  mobile: 320,
  tablet: 768,
  desktop: 1024,
  large: 1920
};

/**
 * 响应式图片组件
 * @param {Object} props
 * @param {string} props.src - 默认图 URL
 * @param {Array<{width: number, url: string}>} props.sources - 多档图列表
 * @param {string} props.alt - 替代文本
 * @param {number} props.width - 原始宽度
 * @param {number} props.height - 原始高度
 * @param {'lazy'|'eager'} props.loading - 加载策略
 * @param {'high'|'low'|'auto'} props.fetchPriority - 优先级
 * @param {string} props.className - CSS 类名
 */
export function ResponsiveImage({
  src,
  sources = [],
  alt = '',
  width,
  height,
  loading = 'lazy',
  fetchPriority = 'auto',
  className = '',
  sizes = '100vw'
}) {
  const [supportedFormats, setSupportedFormats] = useState({ avif: false, webp: false });

  useEffect(() => {
    // 检测浏览器支持的格式
    Promise.all([
      checkFormatSupport('image/avif'),
      checkFormatSupport('image/webp')
    ]).then(([avif, webp]) => {
      setSupportedFormats({ avif, webp });
    });
  }, []);

  const srcset = sources.map((s) => `${s.url} ${s.width}w`).join(', ');

  return (
    <picture>
      {supportedFormats.avif && sources.length > 0 && (
        <source
          srcset={sources.map((s) => s.url.replace(/\.(jpg|jpeg|png)$/, '.avif') + ` ${s.width}w`).join(', ')}
          sizes={sizes}
          type="image/avif"
        />
      )}
      {supportedFormats.webp && sources.length > 0 && (
        <source
          srcset={sources.map((s) => s.url.replace(/\.(jpg|jpeg|png)$/, '.webp') + ` ${s.width}w`).join(', ')}
          sizes={sizes}
          type="image/webp"
        />
      )}
      {sources.length > 0 && (
        <source srcset={srcset} sizes={sizes} />
      )}
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        loading={loading}
        decoding="async"
        fetchpriority={fetchPriority}
        className={className}
        style={{ aspectRatio: width && height ? `${width} / ${height}` : undefined }}
      />
    </picture>
  );
}

function checkFormatSupport(mimeType) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => resolve(img.width > 0);
    img.onerror = () => resolve(false);
    img.src = `data:${mimeType};base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=`;
  });
}

// 使用示例
// <ResponsiveImage
//   src="/img/photo-800.jpg"
//   sources={[
//     { width: 320, url: '/img/photo-320.jpg' },
//     { width: 640, url: '/img/photo-640.jpg' },
//     { width: 1024, url: '/img/photo-1024.jpg' },
//     { width: 1920, url: '/img/photo-1920.jpg' }
//   ]}
//   alt="示例"
//   width={1920}
//   height={1280}
//   sizes="(max-width: 768px) 100vw, 50vw"
//   fetchPriority="high"
// />

4.7 LQIP(低质量占位符)

<!-- 1. 使用 BlurHash 生成占位符 -->
<div class="img-wrapper" style="background: url('data:image/svg+xml,...blurhash') center/cover;">
  <img
    src="photo.jpg"
    alt="低质量占位符示例"
    width="800"
    height="600"
    loading="lazy"
    decoding="async"
    onload="this.parentElement.classList.add('loaded')"
  />
</div>

<style>
  .img-wrapper { position: relative; overflow: hidden; }
  .img-wrapper img {
    opacity: 0;
    transition: opacity 0.3s ease;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  .img-wrapper.loaded img { opacity: 1; }
</style>

4.8 内容图片与背景图片选择

<!-- 内容图片(语义化,应使用 <img>) -->
<article>
  <img src="article-hero.jpg" alt="文章主图:城市夜景" width="1600" height="900" />
  <p>正文内容...</p>
</article>

<!-- 装饰图片(应使用 CSS background) -->
<style>
  .divider {
    background-image: url('pattern.png');
    height: 20px;
  }
</style>
<div class="divider" role="presentation"></div>

4.9 图片热区 map/area(知道即可)

<map> + <area> 可以在一张图片上划分多个可点击区域(矩形、圆形、多边形),例如地图导航、商品示意图。它属于低频特性,知道”存在且长什么样”即可;完整用法(shape/coords 坐标计算、响应式适配)见专项 420-HTML5ImageMapArea:

<img src="floor-map.png" alt="楼层地图" usemap="#floors" />
<map name="floors">
  <area shape="rect" coords="10,10,120,90" href="/floor/1" alt="一层" />
  <area shape="circle" coords="200,150,40" href="/floor/2" alt="二层" />
</map>

讲解:

  1. usemap="#名字" 把图片与 <map> 关联,name 必须一致。
  2. coords 的取值随 shape 变化:rect 是”左上、右下”两组坐标,circle 是”圆心、半径”。
  3. 每个 <area> 都可以带 alt 与 href,行为类似链接,也支持 target。
  4. 无障碍提醒:热区必须写 alt,否则读屏用户不知道图片上有哪些入口。

5. 对比分析

5.1 图像格式对比

格式压缩类型透明度动画颜色深度压缩率浏览器支持适用场景
JPEG有损不支持不支持24 bit基准100%照片
PNG无损8 bit alpha不支持48 bit较低100%图标、透明图
GIF无损1 bit支持8 bit低100%简单动画(已过时)
WebP有损/无损8 bit alpha支持24 bit-30%97%+通用替代
AVIF有损/无损8/12 bit alpha支持24/48 bit-50%92%+高压缩照片
HEIC有损/无损8 bit alpha支持24 bit-50%iOS/macOS only苹果生态
JPEG XL有损/无损8 bit alpha支持24/32 bit-60%~70%下一代格式
SVG矢量支持支持无限无损100%图标、Logo

5.2 srcset vs picture

维度srcset + sizes<picture> + <source>
控制粒度浏览器自动选择开发者显式控制
艺术指导不支持支持(不同图裁切)
格式协商不支持支持(type 属性)
媒体查询仅 sizes 中声明完整 media 属性
代码复杂度低中
适用场景同图不同分辨率不同图、不同格式

5.3 原生懒加载 vs IntersectionObserver

维度loading="lazy"IntersectionObserver
实现复杂度极低(一行属性)中(JS 代码)
兼容性96%+97%+
触发精度浏览器决定(默认 1250px)开发者可调
SSR 友好是否(需 hydration)
占位符不支持支持
推荐场景一般懒加载复杂场景(LQIP/SQIP)

5.4 响应式图片 vs CSS background-image

维度<img srcset>CSS background-image + image-set()
语义化内容图片装饰图片
SEO索引(alt)不索引
可访问性屏幕阅读器识别不识别
LCP 计入是是(部分浏览器)
懒加载原生支持需 JS
推荐场景内容图片装饰背景

5.5 与 React/Vue 组件方案对比

维度原生 HTML <picture>Next.js <Image>Nuxt <NuxtImg>
自动生成多档否(需手动)是(构建期)是(构建期)
LCP 优化手动自动自动
格式协商手动自动(WebP/AVIF)自动
占位符手动自动(blur)自动
学习成本低中中

6. 常见陷阱与最佳实践

6.1 性能陷阱

陷阱 7.1.1:未设置 width/height 导致 CLS

<!-- 错误:未声明尺寸,加载后跳版 -->
<img src="photo.jpg" alt="未声明尺寸" />

<!-- 正确:声明尺寸,浏览器预留盒子 -->
<img src="photo.jpg" alt="声明尺寸" width="800" height="600" />

陷阱 7.1.2:所有图片都用 loading=“lazy”

<!-- 错误:首屏 LCP 图片懒加载,损害 LCP -->
<img src="hero.jpg" alt="首屏" loading="lazy" />

<!-- 正确:首屏图片立即加载 + 高优先级 -->
<img src="hero.jpg" alt="首屏" loading="eager" fetchpriority="high" decoding="async" />

陷阱 7.1.3:srcset 与 sizes 描述符混用

<!-- 错误:w 与 x 混用 -->
<img srcset="small.jpg 400w, large.jpg 2x" alt="混用" />

<!-- 正确:统一使用宽度描述符 -->
<img srcset="small.jpg 400w, large.jpg 800w" sizes="(max-width: 600px) 100vw, 50vw" alt="统一" />

6.2 可访问性陷阱

陷阱 7.2.1:装饰图片使用非空 alt

<!-- 错误:屏幕阅读器朗读冗余 -->
<img src="divider.png" alt="装饰分隔线" />

<!-- 正确:装饰图片使用空 alt -->
<img src="divider.png" alt="" role="presentation" />

陷阱 7.2.2:内容图片缺少 alt

<!-- 错误:屏幕阅读器朗读文件名 -->
<img src="chart-2024.png" />

<!-- 正确:描述图片内容 -->
<img src="chart-2024.png" alt="2024 年销售柱状图,第三季度销售额 500 万元" />

6.3 SEO 陷阱

陷阱 7.3.1:图片无 alt 影响图片搜索排名

<!-- 错误:Google 图片搜索无法索引 -->
<img src="product.jpg" />

<!-- 正确:包含关键词的 alt -->
<img src="product.jpg" alt="蓝色棉质 T 恤 - 男装春秋款" />

陷阱 7.3.2:图片文件名不友好

<!-- 错误:随机文件名 -->
<img src="IMG_20240115_abc123.jpg" alt="风景" />

<!-- 正确:描述性文件名 -->
<img src="mount-huang-sunrise-2024.jpg" alt="黄山日出 2024" />

6.4 格式选择最佳实践

<!-- 照片:优先 AVIF → WebP → JPEG -->
<picture>
  <source srcset="photo.avif" type="image/avif" />
  <source srcset="photo.webp" type="image/webp" />
  <img src="photo.jpg" alt="照片" />
</picture>

<!-- 图标/Logo:优先 SVG -->
<img src="logo.svg" alt="Logo" width="200" height="50" />

<!-- 透明图:优先 WebP → PNG -->
<picture>
  <source srcset="icon.webp" type="image/webp" />
  <img src="icon.png" alt="透明图标" />
</picture>

<!-- 动画:优先 WebP/AVIF → MP4(视频替代 GIF) -->
<video autoplay muted loop playsinline width="400" height="300">
  <source src="animation.mp4" type="video/mp4" />
</video>

6.5 CDN 与缓存策略

# .htaccess / nginx.conf
<FilesMatch "\.(jpg|jpeg|png|webp|avif|svg)$">
  Header set Cache-Control "public, max-age=31536000, immutable"
  Header set Vary "Accept"
</FilesMatch>

# 根据 Accept 头自动协商格式(Apache)
RewriteEngine On
RewriteCond %{HTTP:Accept} image/avif
RewriteCond %{REQUEST_URI} \.(jpg|jpeg|png)$
RewriteRule ^(.*)\.(jpg|jpeg|png)$ $1.avif [L,T=image/avif]

7. 工程实践

7.1 构建工具:自动生成多档图

Sharp(Node.js):

// scripts/generate-responsive-images.js
const sharp = require('sharp');
const fs = require('fs/promises');
const path = require('path');

const SIZES = [320, 480, 640, 800, 1024, 1280, 1600, 1920, 2560];
const FORMATS = ['webp', 'avif'];

async function generateResponsive(inputDir, outputDir) {
  const files = await fs.readdir(inputDir);
  for (const file of files) {
    if (!file.match(/\.(jpg|jpeg|png)$/i)) continue;
    const inputPath = path.join(inputDir, file);
    const name = path.parse(file).name;

    for (const size of SIZES) {
      const image = sharp(inputPath).resize(size);
      // 原格式
      await image.jpeg({ quality: 80 }).toFile(path.join(outputDir, `${name}-${size}.jpg`));
      // WebP
      await image.clone().webp({ quality: 80 }).toFile(path.join(outputDir, `${name}-${size}.webp`));
      // AVIF
      await image.clone().avif({ quality: 60 }).toFile(path.join(outputDir, `${name}-${size}.avif`));
    }
  }
}

generateResponsive('./src/images', './public/img').catch(console.error);

Webpack 集成(image-webpack-loader + responsive-loader):

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpg|jpeg|png)$/i,
        use: [
          {
            loader: 'responsive-loader',
            options: {
              sizes: [320, 640, 960, 1280, 1920],
              format: 'webp',
              quality: 80,
              name: 'img/[name]-[width].[ext]'
            }
          }
        ]
      }
    ]
  }
};

Vite 集成(vite-imagetools):

// vite.config.js
import { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';

export default defineConfig({
  plugins: [
    imagetools({
      defaultDirectives: (url) => {
        if (url.searchParams.has('responsive')) {
          return new URLSearchParams({
            w: '320;640;1024;1920',
            format: 'webp;avif',
            as: 'srcset'
          });
        }
        return new URLSearchParams();
      }
    })
  ]
});

7.2 Next.js <Image> 最佳实践

// Next.js 14+
import Image from 'next/image';

export default function Article() {
  return (
    <Image
      src="/photo.jpg"
      alt="示例"
      width={1600}
      height={900}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      priority={true}  // 首屏 LCP 图
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

7.3 调试技巧

Chrome DevTools:

  1. Network → Img 过滤:查看图片请求与字节数。
  2. Application → Images:浏览页面所有图片资源。
  3. Lighthouse:审计图片优化建议。
  4. Rendering → Image rendering:检查图片解码耗时。
  5. Performance:录制图像解码与渲染时间线。

调试代码:

// 检测浏览器实际加载的图片 URL
document.querySelectorAll('img').forEach((img) => {
  console.log({
    src: img.src,
    currentSrc: img.currentSrc,
    naturalWidth: img.naturalWidth,
    naturalHeight: img.naturalHeight,
    displayWidth: img.clientWidth,
    displayHeight: img.clientHeight,
    dpr: window.devicePixelRatio
  });
});

// 检测 AVIF/WebP 支持
Promise.all([
  fetch('data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUI=').then(r => r.ok),
  fetch('data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA').then(r => r.ok)
]).then(([avif, webp]) => console.log({ avif, webp }));

7.4 Lighthouse 性能审计

Lighthouse 提供 6 项图像相关审计:

  • uses-responsive-images:图片尺寸是否过大(应使用 srcset)。
  • uses-optimized-images:图片是否经过压缩。
  • modern-image-formats:是否使用 WebP/AVIF。
  • uses-rel-preload:LCP 图片是否预加载。
  • offscreen-images:是否启用懒加载。
  • cumulative-layout-shift:图片是否声明尺寸。

7.5 性能优化清单

  • 所有内容图片使用 <img>,装饰图片使用 CSS。
  • 所有 <img> 声明 width 与 height。
  • 使用 srcset + sizes 提供多档图。
  • 使用 <picture> 进行格式协商(AVIF → WebP → JPEG)。
  • 首屏 LCP 图片 loading="eager" fetchpriority="high"。
  • 非首屏图片 loading="lazy"。
  • 添加 <link rel="preload" as="image"> 预加载 LCP 图。
  • 设置 Cache-Control: immutable 长缓存。
  • 设置 Vary: Accept 协商格式。
  • 使用 CDN 自动裁剪与格式转换(如 Cloudinary、Imgix)。

7.6 测试策略

单元测试(Jest + Testing Library):

import { render } from '@testing-library/react';
import { ResponsiveImage } from './ResponsiveImage';

test('应渲染 img 元素', () => {
  const { getByAltText } = render(
    <ResponsiveImage
      src="/photo.jpg"
      sources={[{ width: 800, url: '/photo-800.jpg' }]}
      alt="测试"
      width={800}
      height={600}
    />
  );
  const img = getByAltText('测试');
  expect(img.tagName).toBe('IMG');
  expect(img).toHaveAttribute('src', '/photo.jpg');
});

E2E 测试(Playwright):

test('应正确加载响应式图片', async ({ page }) => {
  await page.setViewportSize({ width: 400, height: 800 });
  await page.goto('https://example.com');
  const img = page.locator('img[alt="响应式"]');
  await expect(img).toBeVisible();
  const currentSrc = await img.evaluate((el) => el.currentSrc);
  expect(currentSrc).toMatch(/photo-400\.(webp|avif|jpg)$/);
});

8. 案例研究

8.1 MDN Web Docs 实践

MDN 文档站点使用 Hugo 静态生成,图片采用 <picture> + WebP/JPEG 格式协商:

<picture>
  <source srcset="screenshot.webp" type="image/webp" />
  <img src="screenshot.png" alt="MDN 截图" width="800" height="600" loading="lazy" />
</picture>

8.2 BBC 新闻

BBC 采用自定义 <noscript> 回退 + JS 懒加载方案(兼容老浏览器),并在 2018 年迁移到原生 loading="lazy":

<img
  src="https://ichef.bbci.co.uk/news/640/cpsprodpb/12345/production/_123456789_photo.jpg"
  srcset="
    https://ichef.bbci.co.uk/news/320/cpsprodpb/12345/production/_123456789_photo.jpg 320w,
    https://ichef.bbci.co.uk/news/640/cpsprodpb/12345/production/_123456789_photo.jpg 640w
  "
  sizes="(min-width: 1008px) 645px, 100vw"
  alt="BBC 新闻图片"
  loading="lazy"
  width="976"
  height="549"
/>

8.3 Amazon 电商

Amazon 采用动态图床服务,根据用户设备与网络生成最优图:

<img
  src="https://m.media-amazon.com/images/I/61abc._AC_UY218_.jpg"
  srcset="
    https://m.media-amazon.com/images/I/61abc._AC_UY218_.jpg 218w,
    https://m.media-amazon.com/images/I/61abc._AC_UY327_.jpg 327w,
    https://m.media-amazon.com/images/I/61abc._AC_UY436_.jpg 436w
  "
  sizes="(max-width: 600px) 50vw, 218px"
  alt="商品图"
  loading="lazy"
/>

8.4 Unsplash

Unsplash 提供 Srcset API,自动生成多档图:

<img
  src="https://images.unsplash.com/photo-12345?w=800"
  srcset="
    https://images.unsplash.com/photo-12345?w=320 320w,
    https://images.unsplash.com/photo-12345?w=640 640w,
    https://images.unsplash.com/photo-12345?w=800 800w,
    https://images.unsplash.com/photo-12345?w=1200 1200w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  alt="Unsplash 照片"
  loading="lazy"
/>

8.5 Cloudinary CDN

Cloudinary 提供 f_auto,q_auto 自动格式与质量协商:

<img
  src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/photo"
  srcset="
    https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_320/photo 320w,
    https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_640/photo 640w,
    https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_1024/photo 1024w
  "
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Cloudinary 示例"
  loading="lazy"
/>

f_auto 自动选择 AVIF/WebP/JPEG,q_auto 自动选择质量参数。


填空题知识点讲解

常见疑问 4:<img> 元素的 currentSrc 属性返回________,即浏览器根据 srcset/sizes 实际选择的图片 URL。

解析讲解:当前实际加载的图片 URL(DOMString 类型)

解析讲解:img.currentSrc 是只读属性,返回浏览器从 srcset 中选择的实际 URL,若未使用 srcset 则返回 src。

常见疑问 5:<picture> 元素必须包含一个________子元素作为回退。

解析讲解:<img> 元素

解析讲解:<picture> 必须以 <img> 子元素结尾,作为所有 <source> 都不匹配时的回退,也是无障碍技术与 SEO 索引的入口。

编程题知识点讲解

常见疑问 6:实现一个 Node.js 脚本,自动为 public/img/ 目录下所有 JPEG 图片生成 320/640/1024/1920 四档 WebP 与 AVIF 格式,并输出 manifest.json 记录所有图片的多档 URL。

// scripts/generate-images.js
const sharp = require('sharp');
const fs = require('fs/promises');
const path = require('path');

const SIZES = [320, 640, 1024, 1920];
const INPUT_DIR = './public/img';
const OUTPUT_DIR = './public/img/responsive';
const MANIFEST_PATH = './public/img/manifest.json';

async function generate() {
  await fs.mkdir(OUTPUT_DIR, { recursive: true });
  const files = (await fs.readdir(INPUT_DIR)).filter((f) => f.match(/\.jpe?g$/i));
  const manifest = {};

  for (const file of files) {
    const name = path.parse(file).name;
    const inputPath = path.join(INPUT_DIR, file);
    manifest[name] = { webp: [], avif: [], jpg: [] };

    for (const size of SIZES) {
      const image = sharp(inputPath).resize(size, null, { withoutEnlargement: true });

      // WebP
      const webpPath = path.join(OUTPUT_DIR, `${name}-${size}.webp`);
      await image.clone().webp({ quality: 80 }).toFile(webpPath);
      manifest[name].webp.push({ width: size, url: `/img/responsive/${name}-${size}.webp` });

      // AVIF
      const avifPath = path.join(OUTPUT_DIR, `${name}-${size}.avif`);
      await image.clone().avif({ quality: 60 }).toFile(avifPath);
      manifest[name].avif.push({ width: size, url: `/img/responsive/${name}-${size}.avif` });

      // JPEG fallback
      const jpgPath = path.join(OUTPUT_DIR, `${name}-${size}.jpg`);
      await image.clone().jpeg({ quality: 80, progressive: true }).toFile(jpgPath);
      manifest[name].jpg.push({ width: size, url: `/img/responsive/${name}-${size}.jpg` });
    }

    console.log(`Generated ${SIZES.length * 3} variants for ${file}`);
  }

  await fs.writeFile(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
  console.log(`Manifest written to ${MANIFEST_PATH}`);
}

generate().catch(console.error);

常见疑问 7:实现一个 React Hook useResponsiveImage,根据当前视口宽度返回最优图片 URL。要求:

  • 输入:候选图列表 [{width, url}]。
  • 输出:最优 URL 与 naturalWidth。
  • 监听视口 resize,但使用 debounce 200ms。
// useResponsiveImage.js
import { useState, useEffect } from 'react';

export function useResponsiveImage(sources, defaultUrl) {
  const [best, setBest] = useState({ url: defaultUrl, width: 0 });

  useEffect(() => {
    if (!sources || sources.length === 0) return;

    const choose = () => {
      const vw = window.innerWidth;
      const dpr = window.devicePixelRatio || 1;
      const target = vw * dpr;

      // 选择 ≥ target 的最小候选,若无则选最大的
      const sorted = [...sources].sort((a, b) => a.width - b.width);
      const candidate = sorted.find((s) => s.width >= target) || sorted[sorted.length - 1];
      setBest({ url: candidate.url, width: candidate.width });
    };

    choose();

    let timer;
    const onResize = () => {
      clearTimeout(timer);
      timer = setTimeout(choose, 200);
    };
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('resize', onResize);
      clearTimeout(timer);
    };
  }, [sources]);

  return best;
}

// 使用示例
// const { url, width } = useResponsiveImage([
//   { width: 320, url: '/img/photo-320.jpg' },
//   { width: 640, url: '/img/photo-640.jpg' },
//   { width: 1024, url: '/img/photo-1024.jpg' }
// ], '/img/photo-320.jpg');

11.1 书籍

  • “Image Optimization”, Addy Osmani, 2020, O’Reilly Media, ISBN 978-1492055388.
  • “High Performance Browser Networking”, Ilya Grigorik, 2016, O’Reilly Media, ISBN 978-1491901266.
  • “Web Performance in Action”, Jeremy Wagner, 2017, Manning Publications, ISBN 978-1617293375.
  • “High Performance Images”, Colin Bendell et al., 2016, O’Reilly Media, ISBN 978-1491925795.

11.2 论文

  • “AV1 Image File Format (AVIF)”, Cyril Concolato, MMSys 2020.
  • “JPEG XL Next-Generation Image Compression”, J. Sneyers et al., ICASSP 2022.
  • “A Study on WebP Image Compression”, J. Bankoski et al., SPIE 2011.

11.4 开源项目

11.5 课程


附录 A:浏览器兼容性矩阵

特性ChromeFirefoxSafariEdgeOpera
<img> 基础1+1+1+12+1+
srcset 属性34+38+8+12+21+
sizes 属性34+38+8+12+21+
<picture> 元素38+38+9.1+12+25+
loading="lazy"76+75+15.4+79+62+
decoding="async"65+63+14+79+52+
fetchpriority101+132+17+101+87+
AVIF 格式85+86+16.4+91+71+
WebP 格式32+65+14+18+19+
JPEG XL(flag)91+ flag90+ flag不支持91+ flag77+ flag
image-set()21+ -webkit88+14+ -webkit79+15+ -webkit

数据来源:MDN Browser Compatibility Data (BCD), 2024 年 7 月更新。

附录 B:术语表

术语英文释义
设备像素比Device Pixel Ratio (DPR)物理像素与 CSS 像素的比值
艺术指导Art Direction不同视口下使用不同裁切/构图的图片
格式协商Format Negotiation浏览器根据支持的格式选择最优图片
懒加载Lazy Loading图片进入视口附近时才下载
低质量占位符LQIP (Low Quality Image Placeholder)加载前显示的低分辨率模糊图
最大内容绘制Largest Contentful Paint (LCP)视口内最大元素的渲染时刻
累积布局偏移Cumulative Layout Shift (CLS)页面可见元素位置意外变化的累积分数
离散余弦变换Discrete Cosine Transform (DCT)JPEG 等格式使用的有损压缩基础变换
内容分发网络Content Delivery Network (CDN)边缘节点缓存图片,加速全球访问

附录 C:相关规范文档

  • HTML Living Standard (WHATWG, 持续更新) - §4.8.3 img element, §4.8.4 picture element
  • CSS Image Values and Replaced Content Module Level 4 (W3C, 2024) - 定义 image-set() 函数
  • AV1 Bitstream & Decoding Process Specification (AOM, 2019) - AVIF 编码基础
  • WebP Container Specification (Google, 2024) - WebP 格式定义
  • JPEG XL Standard (ISO/IEC 18181-1:2022) - JPEG XL 编码规范

本文档遵循 MIT/Stanford/CMU 教学水准,结合 WHATWG HTML Living Standard 与 W3C HTML5.3 规范,系统呈现 HTML5 图像与响应式图片 API 的设计原理与工程实践。如需进一步学习,请参阅延伸阅读章节列出的书籍、论文与课程。

img 元素

图像标签 <img src="<URL>" alt="<替代文本>" [width="<宽>"] [height="<高>"] [loading="lazy|eager"] [decoding="async|sync|auto"] [srcset] [sizes] />

<!-- 基础图像 -->
<img src="photo.jpg" alt="美丽的风景" width="800" height="600" />

<!-- 延迟加载 -->
<img src="photo.jpg" alt="照片" loading="lazy" />

<!-- 异步解码 -->
<img src="large.jpg" alt="大图" decoding="async" />

<!-- 错误回退 -->
<img src="photo.jpg" alt="照片" onerror="this.src='fallback.jpg'" />
属性作用
src图像 URL
alt替代文本(必填,无障碍)
width宽度(像素)
height高度(像素)
loadinglazy 懒加载 / eager 立即加载
decoding解码方式 async/sync/auto
srcset多源图像列表
sizes不同视口下的显示尺寸
referrerpolicyReferer 策略
usemap关联 image map

响应式图片 srcset

宽度描述符 <img srcset="<URL> <宽度>w, <URL> <宽度>w, ..." />

<!-- 浏览器根据视口自动选择合适尺寸 -->
<img
  src="small.jpg"
  srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
  alt="响应式图片"
/>

像素密度描述符 <img srcset="<URL> 1x, <URL> 2x, <URL> 3x" />

<!-- Retina 屏适配 -->
<img
  src="photo.jpg"
  srcset="photo.jpg 1x, photo@2x.jpg 2x, photo@3x.jpg 3x"
  alt="高分辨率图片"
/>

sizes 属性

显示尺寸声明 <img srcset="..." sizes="<媒体查询> <尺寸>, ... <默认尺寸>" />

<img
  src="photo.jpg"
  srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw"
  alt="响应式图片"
/>

选择宽度计算 选择宽度 = sizes 计算值 × 设备像素比

// JavaScript 读取当前显示的图片
const img = document.querySelector('img');
console.log(img.currentSrc); // 当前加载的 URL

picture 元素

多格式回退 <picture><source srcset="<URL>" type="<MIME>" />...<img src="<URL>" alt="<替代>" /></picture>

<!-- 按格式优先级回退 -->
<picture>
  <source srcset="photo.avif" type="image/avif" />
  <source srcset="photo.webp" type="image/webp" />
  <img src="photo.jpg" alt="照片" width="800" height="600" />
</picture>

按媒体查询切换 <source media="<媒体查询>" srcset="<URL>" />

<!-- 不同视口加载不同图片 -->
<picture>
  <source media="(min-width: 1200px)" srcset="wide.jpg" />
  <source media="(min-width: 768px)" srcset="medium.jpg" />
  <img src="small.jpg" alt="响应式图片" />
</picture>

<!-- 同时指定宽度和格式 -->
<picture>
  <source
    media="(min-width: 1200px)"
    srcset="large.avif 1200w"
    type="image/avif"
  />
  <source
    media="(min-width: 768px)"
    srcset="medium.webp 768w"
    type="image/webp"
  />
  <img src="small.jpg" alt="照片" />
</picture>

图片格式对照

格式压缩类型透明度动画压缩率浏览器支持
JPEG有损不支持不支持中等全部
PNG无损支持不支持较低全部
WebP有损/无损支持支持高97%+
AVIF有损/无损支持支持最高92%+
SVG矢量支持支持—全部
GIF无损支持支持低全部
APNG无损支持支持中等95%+

图片预加载

link preload <link rel="preload" as="image" href="<URL>" [type="<MIME>"] [imagesrcset] [imagesizes] />

<!-- 预加载关键图片 -->
<link rel="preload" as="image" href="hero.webp" type="image/webp" />

<!-- 预加载响应式图片 -->
<link
  rel="preload"
  as="image"
  href="small.webp"
  imagesrcset="small.webp 400w, medium.webp 800w, large.webp 1200w"
  imagesizes="100vw"
/>

image map 图像映射

usemap 关联映射 <img src="<URL>" usemap="#<map名称>" alt="<替代>" /> + <map name="<名称>">...<area>...</map>

<img src="map.png" alt="地图" usemap="#workmap" width="400" height="300" />

<map name="workmap">
  <area shape="rect" coords="34,44,270,350" alt="区域1" href="area1.html" />
  <area shape="circle" coords="337,300,44" alt="区域2" href="area2.html" />
  <area shape="poly" coords="140,21,180,40,150,80" alt="区域3" href="area3.html" />
</map>
shape 值coords 含义
rectx1,y1,x2,y2
circlecenter-x,center-y,radius
polyx1,y1,x2,y2,…,xn,yn
default整个区域

性能优化技巧

宽高属性防止布局跳动

<!-- 指定 width/height,CSS 用比例缩放 -->
<img
  src="photo.jpg"
  alt="照片"
  width="800" height="600"
  style="width: 100%; height: auto;"
/>

fetchpriority 优先级

<!-- 关键首屏图片 -->
<img src="hero.jpg" alt="主图" fetchpriority="high" />

<!-- 非关键图片 -->
<img src="icon.png" alt="图标" fetchpriority="low" loading="lazy" />

figure 与 figcaption

<figure>
  <img src="chart.png" alt="销售数据图表" />
  <figcaption>图1:2026年上半年销售数据</figcaption>
</figure>

动手试试

入门版(必做)

  1. 准备三张同内容、不同宽度的图片(400/800/1200 像素),用 srcset + sizes 接入页面;
  2. 打开浏览器开发者工具,把设备模拟切换到 iPhone 和桌面,观察网络面板中下载了哪张图;
  3. 给首屏大图加 loading="eager"(默认),给长列表图片加 loading="lazy"。

进阶版(选做)

  1. 用 <picture> 实现“手机显示竖图、桌面显示横图”的艺术指导场景;
  2. 用 preload 预加载首屏 Hero 图,对比 Lighthouse 的 LCP 分数;
  3. 用 2x 描述符为头像做高清适配。

核心知识点

一句话记住响应式图片:srcset 给候选,sizes 说宽度,picture 换场景;高清用 2x,懒加载用 lazy。

  • srcset + sizes 解决“同一张图选多大”;
  • <picture> + <source media> 解决“不同场景换图”;
  • DPR 为 2 的屏幕需要 2 倍图才清晰;
  • loading="lazy" 延迟加载,decoding="async" 异步解码;
  • 所有图片必须写 alt,装饰图留空。

注意事项与改进建议

问题点说明改进方案
图片缺 width/height布局抖动(CLS)显式给出尺寸或使用宽高比占位
全部图片 lazy首屏图片延迟加载反而拖慢 LCP首屏用默认 eager,长列表用 lazy
srcset 与 sizes 混用描述符w 与 x 混用行为不可预测二选一,统一用 w + sizes
装饰图写非空 alt读屏播报无意义内容装饰图 alt=""
内容图缺 alt图片搜索与无障碍双损失描述性 alt
过度预加载抢占带宽只 preload 首屏关键图

扩展学习

  • 格式对比:AVIF/WebP/JPEG 的选择见 javascript/450-FetchApiWebStreams(JavaScript 模块);
  • 性能指标:javascript/510-CoreWebVitalsAndPerformanceMetrics 中 LCP/CLS 的测量;
  • 懒加载原理:javascript/430-WebAPIBrowserInterface 中 IntersectionObserver 实现;
  • 工程化:html5/380-CriticalRenderingPathAndResourceLoading 资源加载策略;
  • 组件方案:React 的 next/image 或 Vue 的 v-img 自动生成多档图。