Nicholas Clooney

用 Eleventy Img 升级响应式图片

所属系列

最初的问题

我发现,小屏幕上的截图看起来变形了,尽管原始素材本身很清晰。大概就是这样……

手机上图片被拉伸的截图

Markdown 文章使用普通 <img> 标签,只指定了 height="300"。布局变窄时,浏览器会缩小图片宽度以适应内容栏,这是 Tachyons 默认 img { max-width: 100%; } 的行为,但_通过属性设置_的高度仍锁定在 300 像素。浏览器将显式 HTML 属性的优先级视为高于 CSS 后备值,所以为了满足固定高度,图片在垂直方向被拉伸。移除写死的高度,变形就消失了。

我们希望解决方案能够:

  • 无需在 Markdown 中手动填写尺寸,也能保持宽高比。
  • 向移动设备提供更小的文件。
  • 为 WebP、AVIF 等现代格式打好基础,不用手写 <picture> 标记。

于是,Eleventy Img 登场了。

目录

为什么选择 @11ty/eleventy-img

这个插件只需很少配置,就能生成可用于生产的响应式图片:

  • 从一张源图生成多种宽度和格式,包括 WebP、JPEG,以及需要时的 AVIF。
  • 在构建时压缩,让浏览器下载更少的数据。
  • 缓存输出,源图和配置都没变时跳过重复处理。
  • 输出正确标记,包括 <picture>、srcset、sizes 和宽高属性,保留图片固有比例,减少布局偏移。
  • 方便未来升级:启用新格式,只需在 formats 数组中多加一项。

我们如何接入 Eleventy Img

最初,我们把 Markdown 中的 <img> 换成 {% image %} 短代码。虽然可行,但作者每次放截图都得记住一个自定义标签。后来发现,更适合这个项目的是 Eleventy Img 的 HTML 转换插件。

转换插件扫描渲染后的 HTML,找到每一个 <img>,再将其改写为完整的 <picture> 元素。我们的配置(eleventy.config.js)设置了:

  • formats: ["avif", "webp", "jpeg"],让现代浏览器优先拿到更小的文件。
  • widths: [320, 640, 960, 1280],覆盖内容栏的半倍、1 倍和 2 倍尺寸。
  • 在 htmlOptions.imgAttributes 中设置 loading="lazy"、decoding="async",以及与布局对应的 sizes 字符串((width <= 30em) 100vw, 75vw):手机上占满屏宽,达到 Tachyons 的 -ns 断点后,大约占视口宽度的 75%。

本地开发时,Eleventy 通过按需处理的 /.11ty/image/ 端点提供派生图片;生产构建则把最终文件写入 _site/img/。由于转换发生在 Markdown 渲染之后,作者可以继续使用普通 HTML 或 Markdown 图片,同时享受响应式标记带来的好处。

响应式图片标记如何工作

主要靠两个属性:

  • srcset 列出候选文件,每个带一个宽度描述符,例如 640w。
  • sizes 告诉浏览器,图片在布局中会显示多宽。如果省略,浏览器会假定为 100vw,可能下载过大的图片。

经典媒体查询语法

sizes="(max-width: 600px) 100vw, 600px"

从左往右读:视口宽度不超过 600px 时,图片占满视口宽度;否则,将宽度限制在大约 600px。

范围语法(Media Queries Level 4)

sizes="(width <= 37.5em) 100vw, 37.5em"

较新的范围语法用类似数学的比较替代 max-width。使用 em,可以让断点与文字大小关联起来;默认基准为 16px 时,37.5em 约等于 600px,用户缩放页面时也能更自然地适配。

选择合适的宽度

确定最大显示宽度后,再生成几个有用的派生尺寸。一个简单的经验是最大值的 0.5 倍、1 倍和 2 倍。我们的内容栏宽度大约为 640px,因此生成 [320, 640, 960, 1280]。Eleventy Img 会自动舍弃超过源图尺寸的选项,所以可以多给一些较大的尺寸,不必担心放大后模糊。

这些宽度进入 srcset 后,浏览器会根据当前设备像素比和视口宽度,选择仍足够清晰的最小文件。再配上合适的 sizes,手机用户下载的数据会大幅减少,视网膜屏幕也仍然能得到清晰图片。

确保宽高比正确的保护措施

  • 始终提供有意义的 alt 文本。忘记时 Eleventy Img 会报错,避免把无障碍缺陷带到线上。
  • 保留生成的 width 和 height 属性;它们描述最大派生图的固有尺寸,浏览器据此预留布局空间。
  • 如果文章需要针对不同屏幕调整构图,比如手机上裁成正方形,就使用短代码,在 <picture> 中按断点指定来源。转换插件很适合默认情况,但处理这些特殊需求不够灵活。
  • 我们加了一条小小的 CSS 覆盖(img { height: auto; }),确保窄内容栏里图片缩小时,转换生成的显式高度不会压过响应式布局。

最终效果

Markdown 文章里仍然只需一个简单的 HTML 图片:

<img
  alt="Example social card generated by the Subspace Builder"
  src="/assets/images/subspace/social-cards/social-cards.png"
/>

转换插件运行后,Eleventy 会输出以下内容,这里摘自开发服务器:

<picture>
  <source
    type="image/avif"
    srcset="
      /img/9NWum2aR9G-320.avif 320w,
      /img/9NWum2aR9G-640.avif 640w,
      /img/9NWum2aR9G-960.avif 960w
    "
    sizes="(width <= 30em) 100vw, 75vw"
  />
  <source
    type="image/webp"
    srcset="
      /img/9NWum2aR9G-320.webp 320w,
      /img/9NWum2aR9G-640.webp 640w,
      /img/9NWum2aR9G-960.webp 960w
    "
    sizes="(width <= 30em) 100vw, 75vw"
  />
  <img
    loading="lazy"
    decoding="async"
    alt="Example social card generated by the Subspace Builder"
    src="/img/9NWum2aR9G-320.jpeg"
    width="960"
    height="504"
    srcset="
      /img/9NWum2aR9G-320.jpeg 320w,
      /img/9NWum2aR9G-640.jpeg 640w,
      /img/9NWum2aR9G-960.jpeg 960w
    "
    sizes="(width <= 30em) 100vw, 75vw"
  />
</picture>

Eleventy 舍弃了 1280px 的派生图,因为源 PNG 最宽只有 1200px。这正是我们想要的。结合转换插件和 CSS 保护,截图在各种视口下都能保持宽高比,并下载适当大小的文件。

推荐阅读

如果你已有一个 Eleventy 项目,不妨把一个写死的 <img> 换成短代码,重新构建,再检查 HTML。结合上下文查看生成的 <picture> 标记,是理解 srcset 与 sizes 如何配合的最快方式。