现代 CSS

图片的优化

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

图片在 Web 中的应用是非常广泛的,而且占用的资源相比于 HTML、CSS 和 JavaScript 资源要大得多。我采用 HTTPArchive 对 Top1000000 (2021-01-01~2021-08-01)做了一个统计

同等基数,把图片相关的信息拿出来

聚划算来说,其中图片占了大约 730.4 KB

图片在 Web 中除了占有重要的一席之地之外,对资源的使用也是非常的大,而且对用户的体验影响也非常大。虽然 <img> (或 CSS 中其他能用<image> 数据类型的属性,比如 background-imagemask-imageborder-image 以及 list-style-image 等)看上去不是很起眼,但图片使用的好坏直接会影响用户的体验,Web 的性能(加载,渲染相关的性能)。或者简单地说,它直接影响 CWV (Core Web Vitals)的几个关键指标,比如 LCPCLSFID

为什么说图片会对 LCP、CLS 和 FID 有着直接影响呢?我们先从 <img> 的使用开始,然后逐步来分析。

图片是很难的

对于很多 Web 开发者而言,把图片放到一个 Web 页面(或 Web 应用)中太简单了。使用 <img> 标签,并且通过 src 指定一个图片来源(本地图片资源,或线上图片资源):

<!-- 引用本地图片(相对路径) -->
<img src="taobao.png" />

<!-- 引用线上图片(绝对路径) -->
<img src="https://picsum.photos/400?random=4" />

或者说,把图片在CSS中当作背景被用于 Web 中:

<!-- HTML -->
<div class="container"></div>

<style>
    .container {
        background-image: url("https://picsum.photos/400?random=4"); /* url() 可以引用本地和线上的图片资源 */
    }
</style>  

事实上,要在 Web 上用好图片并不是件易事,大部分 Web 开发者在使用图片时往往选择性的忽略很多必做的事情,比如:

  • 确保在 <img>alt 添加描述图片的信息
  • 如果你想用可见的文本来描述图片信息,应该使用 <figure> 标签来包裹 <img> ,并使用 <figcaption> 给图片提供显示描述文本信息
  • <picture> 元素中加入 <source> 元素,引入不同格式的图片源
  • 为不同的屏幕尺寸和其他条件提供不同的图片源
  • 对图片的格式有足够的了解,可以在适当的时候使用 SVG 等格式
  • 对你的受众有足够的了解,可以默认使用现代的格式
  • 使用 srcsetsizes 属性来提供不同大小的图片,以便在不同的设备上节省带宽(为了尽可能的高效,创建这些图片变化取决于图片本身,而不是预先确定的尺寸)
  • 优化图片和它的所有变化。尝试使用不同的优化软件,挑选结果最佳的。确保优化后的版本不会意外的比原来尺寸大或质量低
  • 懒加载图片,这样用户就不必下载他们不会看到的图片。HTML 的 loading="lazy" 可以实现图片的懒加载
  • 确保在 <img> 上显式设置 widthheight 属性的值,以便图片在加载前在布局中保留正确的空间(即使布局是自适应的)
  • 将你的图片托管在一个快速的、无 cookie的全球 CDN上。可以是 <link rel="preconnect"><link rel="dns-prefetch"> 到 CDN 域。但不要在开发阶段做任何 CDN 的事情,只在生产阶段做。你的图片的标准来源应该是你自己的服务器,所以你可以根据需要移动 CDN
  • 页面上最大的图片可以提前预加载,比如 <link rel="preload" as="image"> 以获得最佳的 LCP
  • 试着弄清楚什么时候应该使用 decoding="async" (图片解码方式)
  • 在图片没有加载的情况下,要有降级样式
  • 考虑更多令人愉悦的加载前或加载失败的图片,比如小的模糊版本的图片
  • 要有性能监控,以确保大图片不会被使用

可能你会说,无法做到上面列出的(或未列出又对图片有利的优化)。不过现实是,有时候只做其中的一些事情,哪怕得到微小的变化,对于用户来说都是有益的。在社区中,建立在 <img> 基础上的图片组件,比如 Next.js<Image>(用于React环境)和 Nuxt<Image> (用于 Vue环境)都尽量在默认情况下加入这些概念。另外 Eleventy<Image> 组件和 gatsby-plugin-image 都在如何将这些东西自动化并尽可能多地提供这些最佳实践方面受到好评。

图片是如何影响用户体验和 CWV 的核心指标

你可能已经听说过 核心Web指标(CWV,Core Web Vitals)。CWV 是 Google 发布的一套新的用来衡量页面体验的最终指标。简单地说,CWV 是模拟真实的场景,以用户为中心的一系列用户体验指标。主要衡量Web页面可用性的多个维度,分别是 加载时间(Loading) 、交互性(Interactivity) 和 内容加载时的稳定性(Visual Stability)

这三个指标分别是 LCP (对应 Loading)、FID (对应 Interactivity) 和 CLS (对应 Visual Stability)。

上图中,每个指标都有三个等级的划分,绿色部分表示优秀、黄色部分表示一般(略及格),红色部分表示极差。如果你的 Web 页面这三项指标都达到绿色级别,意味着你给用户提供了良好的用户体验。而 Web 中的图片却可以通过多种方式影响 CWV,让这几项指标达不到好的分数

(上图:聚划算首页,氛围图直接拉低了 LCP 的指标,LCP测量用户视窗中最大的内容元素,比如图片、文本。氛围图何时变得可见,直接决定 LCP 是否达标)

在许多现代 Web 体验中,当一个页面完成加载时,图片往往是最大的可见元素。比如氛围图、英雄图(横幅,即 Hero Image)、旋转木马的图片(轮播图片,即 Carousel)、故事图(Stories)和 广告图(Banner)。LCP(Largest Contentful Paint)是 CWV 的重要指标之一,用于衡量用户视窗中最大的内容元素何时可见。例如这里提到的这些图片之一。

注意,LCP 会检测图片(<img><picture>)、SVG矢量图(<svg>)、视频(预览图)、通过 url() 引入的图片资源和包含文字的块元素或行内元素。

这使得浏览器能够确定页面的主要内容何时完成渲染。当图片是最大的内容元素时,该图片的加载速度会影响 LCP。除了应用图片压缩(比如 SquooshSharpImageOptim图片 CDN),并使用现代图片格式外,你可以调整 <img> 元素,以提供最合适的图片响应式版本或图片懒加载。

(上图是 WebPageTest filmstrip中显示的 Largest Contentful Paint,即LCP)

布局的变化(Layout Shifts)会让用户分心。想象一下,当你开始浏览Web页面时,突然间页面上的元素发生了变化,你不得不重新找到变化前浏览的位置。是不是会有一种不适(不舒服)的感觉(体验)。CWV中的 CLS(Cumulative Layout Shift)指标就是用来衡量内容的稳定性,即 衡量用户在整个页面生命周期中发生的布局偏移情况

导致 CLS 的最常见的原因包括没有尺寸的图片,即 <img> 没有显式设置 widthheight 属性的值,这些图片在加载时可能会让其他元素内容移动,比如内容往下移。也就是说,忽略 <img>widthheight 设置意味着浏览器可能无法知道它们加载前保留足够的空间。

(上图是 聚划算移动端CLS)

(上图是聚划算桌面端 CLS)

CLS的动图可以通过 Cumulative Layout Shift Debugger(CLS)Layout Shift Gif Generator来录制。

图片有可能在页面加载时阻塞用户的带宽和 CPU。它们会妨碍关键资源加载,特别是在较慢的网格连接和低端移动设备上,导致带宽饱和。CWV的首次输入延迟 FID (First Input Delay)指标,可以捕捉到用户对 Web 页面的互动性和响应性的第一印象。即 从用户第一次与页面交互(如点击链接、点击按钮或使用自定义的 由 JavaScript 脚本自驱动的控件)到浏览器实际能够开始处理事件的时间间隔。FID 主要作用就是用来衡量页面的交互的性能体验。

换句说,FID衡量的是,当用户认为页面已经完成时,它是否真的已经完成。如果用户点击页面时,浏览器正忙于下载、解析和运行 JavaScript 脚本,那么在浏览器处理事件和触发点击事件之前,会有一个延迟。 FID 测量这种延迟。

比如下面这个示例:

通常情况下,页面加载时会发生多个请求去加载资源(比如 CSS、JavaScript 、字体和图片等),上图中黄色代表此时主线程正在忙碌中。

这个时候其实已经有部分内容加载出来了(FCP),但是距离页面真正可交互时间(TTI:Time to Interactive)的节点还有一段时间。

而此时,如果用户进行了点击或输入操作,主线程实际上会将该任务挂起,在部分资源加载执行完成后才执行该任务。因此,这时候就有了 FID 耗时。

上图中,FID被捕获并显示在控制台中。该页面有一些缓慢的 JavaScript 脚本,在页面加载时阻塞了浏览器的主线程。即 缓慢的 JavaScript 延迟了用户的第一次点击(图中黄色点)。

为此,我们可以通过减少主线程的 CPU 使用率来减少 FID。

Lighthouse

我们可以使用 Lighthouse来确定改进 CWV 的方案。 Lighthouse 是一个开源的自动化工具,用于提高网页的质量。你可以在 Chrome DevTools 调试工具套件中找到它,并针对任何网页运行它。你还可以在 PageSpeed InsightsCIGTmettrixWebpageTest中找到 Lighthouse。

Lighthouse 是一个实验工具。虽然它很适合用来寻找改善用户体验存在的问题(或缺陷),但还是要尽量参考真实环境中的数据,才能更全面了解实际用户的情况。

基础知识

要在一个 Web 页面上放置一张图片,使用 HTML 的 img 标签元素即可。<img> 标签至少需要一个 src 属性,引用图片源:

<!-- 引用本地图片(相对路径) -->
<img src="taobao.png" />

<!-- 引用线上图片(绝对路径) -->
<img src="https://picsum.photos/400?random=4" />

为了确保我们的图片具有可读性,需要在 <img>alt 属性上添加图片的文本描述,当图片无法显示或看到时,它被用作图片的替代品;或者像屏幕阅读器这样的 ATs 访问你的图片时,它就会朗读出 <img>alt 中设置的文本描述(如果未设置,会朗读图片的文件名)。在 <img> 中指定 alt 属性的值,看起来像下面这样:

<!-- 引用本地图片(相对路径) -->
<img src="taobao.png" alt="淘宝,太好逛了吧!" />

如果你想更深入的探究为什么要给 <img> 设置 alt 属性值以及如何更好的设置 alt 值感兴趣的话,可以阅读下面这些教程:

接下来,我们在 <img> 显式设置 widthheight 属性的值来指定图片的宽度和高度(图片的尺寸):

<img src="juhuasuan.jpg"
    alt="聚划算"
    width="117"
    height="30" >

当在图片上指定了 widthheight 时,浏览器就知道要为该图片保留多少空间,直到它被下载。如果没有设置图片的尺寸,就会导致布局变化(CLS),因为浏览器不确定图片需要多少空间。

现代浏览器可以根据图片的 widthheight 属性来设置图片的默认宽高比(Aspect Ratio),所以设置它们来防止布局移动(CLS)是有很价值的。

确定 LCP 的元素

影响 LCP 的元素类型主要有:

  • <img> 元素
  • 内嵌在 <svg> 元素内的 <image> 元素
  • <video> 元素 (使用封面图片(poster 引入的图片)测量 LCP)
  • 通过 url() 函数(而非 CSS 渐变)加载的带有背景图片的元素
  • 包含文本节点或其他行内级文本元素的 块级 元素

Lighthouse 有一个 “LCP” 的审查,可以帮助我们识别页面上哪个元素是 LCP 元素。将鼠标悬停在该元素上,会在主浏览器窗口中突出显示该元素:

如果这个元素是一个 <img> ,这个信息是一个有用的提示,你可以对该图片进行优化(比如优化和压缩图片)。你也可以直接在 Lighthouse 的 LCP 审查区域点击该元素,会直接跳到开发者工具的 元素(Elements) 面板,定位到该元素在 DOM 中的位置:

如果在首屏渲染,加载这些元素(影响 LCP 的元素类型)所需的时间将对 LCP 产生直接影响。有几种方法可以确保尽快加载这些文件:

  • 优化和压缩图片
  • 预加载重要资源
  • 基于网格连接交付不同资源(自适应服务)
  • 使用 Service Worker 缓存资产

正如聚划算首页(其实大多网站来说),在页面加载完毕后,图片会是视图中的最大元素。比如页面的氛围图、横幅广告图和大型轮播图等。改善这些类型的图片加载和渲染所需的时间将直接提升 LCP 的速度。实现方式:

  • 首先考虑不使用图片。如果图片与内容无关,请将其删除
  • 压缩图片
  • 将图片转换为更新的格式,比如 JPEG 2000、JPEG XR 或 WebP 等
  • 使用响应式图片
  • 考虑使用图片 CDN

其实在 Lighthouse 提供的检测报告也会提供针对 LCP 优化的一些建议:

不过我们这篇文章仅关注的是与图片相关的优化。比如聚划算氛围图:

就可以在 <link> 标签使用 preload 对图片进行预加载:

<link rel="preload" as="image" href="wolf.png" />

如果 <img> 中使用了 srcsetsizes 相关特性(实现响应式图片),也可以像下面这样对图片进行预加载:

<link
    rel="preload"
    as="image"
    href="wolf.jpg"
    imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w"
    imagesizes="50vw"
    />

识别来自没有尺寸的图片引起的 CLS

就浏览器渲染图片来说,如果图片没有尺寸(<img> 未显式设置 widthheight),在图片加载完成之前,浏览器是不知道要预留多少空间给图片的。如此一来就会引起累积布局偏移(CLS)。

其实不仅图片元素未设置 widthheight 会引起 CLS,在 Web 中只要和 <img> 元素类似的(可替换元素)元素在没有显式设置 widthheight 都会引起 CLS,比如<iframe><video><object> 等。

如果你的页面中的可替换元素(比如 <img>)未显式设置 widthheight 值时, Lighthouse 的审查报告将会突出显示出这些元素。

而在 Web 中,引起 CLS 较差的最常见的原因主要有:

  • 无尺寸的图片
  • 无尺寸的广告、嵌入 和 iframe
  • 动态注入的内容
  • 导致不可见的文本闪烁(FOIT)和无样式文本闪烁(FOUT)的网格字体(自定义字体,即 @font-face 引入的字体)
  • 在更新 DOM 之前等待网格响应的操作

我们主要来看“无尺寸的图片”这个原因。也就是说,给图片设置宽高又变得重要了。

给图片设置宽高再次变得重要

由于最近浏览器的一些变化,为了防止 CLS 并改善 Web 用户(访问者)的体验,在图片上显式设置宽高变得非常有价值。

Web 性能优化倡导者经常建议为你的图片设置宽高属性以获得最佳性能,以便图片本身被下载之前,使页面能够为图片提供适当的空间。这就避免了图片下载时的布局变化。这也是 CWV 重要指标之一,因为无尺寸的图片是引起 CLS 较差的常见原因之一。比如下面两个视频的效果:

(未显式指定图片宽高)

(已显式指定图片宽高)

为此,我们自己也写了一个简单的示例,页面中放置了大约 208 张图片,而且都没有给 <img> 显式设置 widthheight ,同时没有添加任何处理图片的 CSS 代码;同时另一个示例,显式在 <img> 上设置了 widthheight 。两个示例的对比结果如下:

(上图截自Chrome浏览器隐身模式下)

(Lighthouse 设置图片尺寸对 CLS 的影响)

在 Web 发展的早期阶段,开发者都习惯性的会在 <img> 元素上显式设置 widthheight 属性,从而确保浏览器在开始获取图片前会在页面上预先分配足够的空间。这样可以最大限度地减少重排和重绘。

<img src="taobao.png" alt="淘宝,太好逛了吧!" width="170" height="30" />

你可能会注意到,上方的 width 和 height 不包括单位。这些“像素”尺寸可以确保一块 170 x 30 的保留区域。无论图片的真实尺寸是否匹配,该图片都会被拉伸成保留区域的大小。

注意,<img> 元素的 widthheight 属性的值一般是不带单位的值,在不带单位的时候,默认为 px 值。除此之外可以在 widthheight 设置带有 px% 的值,其中 px 是一个固定尺寸值,而 % 是相对值,相对于 <img> 父容器宽高进行计算:

<img src="taobao.png" alt="淘宝,太好逛了吧!" width="170" height="30" />
<img src="taobao.png" alt="淘宝,太好逛了吧!" width="170px" height="30px" />
<img src="taobao.png" alt="淘宝,太好逛了吧!" width="50%" height="30%" />

但设置为其他单位值时,客户端在解析时会把单位值忽略,比如:

<img src="https://picsum.photos/500?random=1" alt="" width="50pt" height="50pt">
<img src="https://picsum.photos/500?random=1" alt="" width="50vw" height="50vw">
<img src="https://picsum.photos/500?random=1" alt="" width="50cm" height="50cm">

上面代码中 widthheight 的值单位 ptvwcm 都被忽略,最终解析成 px 值:

如果 <img> 元素的 widthheight 两个属性同时指定的话,必须满足下面条件之一:

  • 指定宽度 - 0.5 ≤ 指定高度 * 目标宽高比率 ≤ 指定宽度 + 0.5
  • 指定高度 - 0.5 ≤ 指定宽度 / 目标宽高比率 ≤ 指定高度 + 0.5
  • 指定宽度 = 指定高度 = 0

其中指定宽度和指定高度是 开发者在 <img> 上显式设置的 widthheight 属性值,目标宽高比率指的是图片原始尺寸(内在尺寸)的宽度和高度的比例。图片在未加载到 Web 页面就有一个尺寸,这个尺寸是设计师导出图片的原始尺寸:

我们可以通过 naturalWidthnaturalHeight 两个 DOM API 来获取图片的原始尺寸。或者在浏览器开发者工具中,将鼠标移动 <img> 元素标签上,开发者工具会显示用户设置的 widthheight ,同时也会显示出图片的原始尺寸(即内在尺寸,Intrinsic):

  • Rendered Size 指的是用户设置的尺寸
  • Intrinsic Size 指的是图片的原始尺寸
  • Rendered Aspect Ratio 指的是 <img> 元素设置的 widthheight 属性值的比例

<img> 元素同时设置 widthheight 属性值,不需要通过任何计算就可以知道图片在浏览器中的渲染尺寸。如果 <img> 元素只显示设置 widthheight 属性的其中一个值时,那么图片在浏览器中的渲染尺寸是需要计算的。其计算原理非常的简单。假设,我们有一张原始尺寸为 600 x 400 的图片:

(图片源:https://picsum.photos/600/400?random=1)

<img> 未显式设置 widthheight 时,浏览器渲染会按图片原始尺寸进行渲染:

<img src="https://picsum.photos/600/400?random=1" alt="">

浏览器渲染出来的结果:

当你显式在 <img> 上设置了 width = 300

<img src="https://picsum.photos/600/400?random=1" alt="" width="300">

这个时候浏览器渲染出来的结果如下:

从上图可以获知,图片渲染出来的高度是 200px 。我们来简单地看其计算原理。示例中的图片原始尺寸是 600 x 400

› nW = 600                               // 图片原始宽度
› nH = 400                               // 图片原始高度
› R = nW / nH = 600 / 400 = 3 / 2 = 1.5  // 图片原始宽高比

显示在 <img> 中设置了 width=“300” ,根据上面的公式可以计算出 height 值:

› R = nW / nH 
› H = W / R = W / nW * nH 
› H = 300 / 600 * 400 = 200

计算出来的结果和浏览器渲染出来的结果是一样的。我们再来看另一情景,<img> 只显式设置 height = "300"

<img src="https://picsum.photos/600/400?random=1" alt="" height="300">

同样根据上面公式来推导出 width 值:

› R = nW / nH 
› nW = 600 
› nH = 400 
› w = R * h = nW / nH * h 
› w = 600 / 400 * 300 = 450

计算出来的 width 和浏览器渲染出来的尺寸是一致的。

另外,在 <img> 元素上同时设置 widthheight 值时,浏览器会计算出其默认的宽高比例,比如:

<img src="https://picsum.photos/600/400?random=1" alt="" width="400" height="200">

这个宽高比例(aspect-ratio)是图片渲染后的宽高比,并不是图片原始尺寸的宽高比。

这个渲染宽高比(Rendered Aspect Ratio)是非常有用的,也是现代浏览器较为聪明之处。这个渲染宽高比将会被运用于 CSS 的 widthheight 的计算。有关于这部分,将单独在图片的宽高比(aspect-ratio)属性中来和大家一起探讨。

同时给 <img> 设置 widthheight 属性时,如果宽高比和原始尺寸宽高比不同时,会造成图片的扭曲(如上图所示);如果给 <img> 同时设置 widthheight 属性值,但超出原始尺寸,即使保持相同的宽高比率,也会造成图片的模糊不清晰:

自从响应式 Web 设计 (Responsive Web Design)到来之后,开发者开始省略 widthheight ,并取而代之的是使用 CSS 来调整图片的大小:

img  {
    width: 100%; /* max-width: 100% */
    height: auto;
}

这种方法的一个缺点是,只有在图片开始下载且浏览器可以确定其尺寸后才能为图片分配空间。随着图片的加载,页面会随着每张图片出现在屏幕上而进行重排,因此导致文本常常突然出现在屏幕上。这与良好的用户体验相差甚远。

这种情况下就需要用到图片宽高比。图片的宽高比是图片宽度与高度的比例。我们通常由冒号分隔的两个数字来表示宽高比,比如 16:94:3x:y 的宽高比表示图片的宽度为 x 个单位,高度为 y 个单位。

也就是说,如果我们知道其中一个维度,就可以确定另一个维度,这个我们在前面已经介绍过了。在知道宽高比的情况下,浏览器就能够进行计算,并为高度和其关联区域预留足够的空间。

在 Web 中除了直接在 <img> 中显式设置 widthheight 来控制图片大小之外,我想更多的 Web 开发者更习惯于在 CSS 中使用 widthheight 来控制图片尺寸,比如:

<!-- HTML --> 
<img src="cat-632x475.jpg" alt="一只灰色的猫" /> 
<img src="cat-632x475.jpg" alt="一只灰色的猫" height="300" /> 
<img src="cat-632x475.jpg" alt="一只灰色的猫" width="300" /> 
<img src="cat-632x475.jpg" alt="一只灰色的猫" height="180" width="300" /> 

// CSS 
img { 
    width: 200px; 
}

示例中只是在 CSS 中显式设置了 width: 200px 。我们来看一下结果:

从上图我们可以得知,CSS 中的 widthheight 会覆盖 <img> 元素中的 widthheight 。如果 <img> 中未显式设置 widthheight 属性,那么 CSS 就会重新计算图片尺寸,这个时候会根据图片的原始尺寸的宽高比来计算:

其计算过程如下:

› R = nW / nH 
› H = W / R = W / nW * nH 
› H = 200 / 600 * 400 = 133.33

如果 <img> 显式设置了 widthheight 属性,那么其计算也是根据图片原始尺寸宽高比例进行计算:

<img src="https://picsum.photos/600/400?random=1" alt="" width="300">
<img src="https://picsum.photos/600/400?random=1" alt="" height="300">

img {
    width: 200px
}

第一张图的 width 属性被 CSS 属性的 width 覆盖,并按下面方式重新计算出 height 值:

› R = nW / nH 
› H = W / R = W / nW * nH 
› H = 200 / 600 * 400 = 133.33

第二张图的 height 的值和 CSS 的 width 值分别保留,并且都同时运用于 <img> 中,只不过会以 CSS 显式设置的 width 为准,计算出来的 width 会被忽略:

› R = nW / nH 
› W = H * R = H * nW / nH 
› W = 300 * 600 / 400 = 450

根据公式,当图片 height=300 时,计算出来的图片 width 应该是 450 ,但由于 CSS 显式设置了 width: 200px ,最终以该值为最终值,因此也就造成了图片的扭曲(渲染出来的宽高比与原始宽高比不相等):

注意,上面这个计算和渲染同样适用于下面这个示例场景:

<img src="https://picsum.photos/600/400?random=1" alt="" width="300">

<img src="https://picsum.photos/600/400?random=1" alt="" height="300">

img {
    height: 300px;
}

// 或
<img src="https://picsum.photos/600/400?random=1" alt="" width="300" height="200">

<img src="https://picsum.photos/600/400?random=1" alt="" width="600" height="300">

img {
    width: 300px;
}

从上面示例,我们不难发现, <img> 中的 widthheight 属性有可能和 CSS 的 widthhegiht 同时共存:

  • <img>width 和 CSS的 height 共存
  • <img>height 和 CSS 的 width 共存
  • <img>widthheight 和 CSS 的 widthheight 共存

<img> 中的某一属性被 CSS 的相同属性覆盖,或者都有可能被 CSS 显式设置的 widthheight 覆盖,就有可能造成图片的扭曲或放大变得模糊。出现这种情况的时候,只需要将另一个属性的值设置为 auto

<img src="https://picsum.photos/600/400?random=1" alt="" width="300" height="200">
<img src="https://picsum.photos/600/400?random=1" alt="" width="600" height="300">

img {
    width: 300px;
    height: auto;
}

现代浏览器会根据图片的(<img>widthheight 属性的值来设置图片的默认宽高比,通过设置这些属性来防止布局偏移(CLS)是非常有价值的。开发者只需要在 <img> 元素上设置 widthheight 即可:

<!-- 设置 600:300,图片宽高比 2:1 -->
<img src="https://picsum.photos/600/400?random=1" alt="" width="600" height="300">

而且所有浏览器的 UA 样式表都会根据元素(<img>)现有的 widthheight 属性添加默认宽高比:

img, input[type="image"], video, embed, iframe, marquee, object, table {
    aspect-ratio: attr(width) / attr(height);
}

现代浏览器会给可替换元素和其他接受 widthheight 的元素添加了 aspect-ratio

浏览器的 UA 样式表如下:

img[Attributes Style] {
    width: 600px;
    aspect-ratio: auto 600 / 300;
    height: 300px;
}

这会在图片加载之前根据 widthheight 属性计算宽高比。样式表在布局计算的一开始就会提供此信息。一旦图片被设定为某一特定宽度(例如,width: 100%),就可以通过宽高比来计算高度。如果你的图片在容器中,可以使用 CSS 将图片大小调整为该容器的宽度。我们需要设置 height: auto 来避免图片高度为某个固定值:

img {
    height: auto;
    width: 100%;
}

在做这方面测试的时候,我发现另一个可能引起 CLS 的原因。比如下面这个示例:

<!-- HTML -->
<div class="box">
    <h2>我是图片</h2>
    <img src="https://picsum.photos/1200/400?random=1" alt="" width="600" height="300">
    <p>测试布局位移</p>
</div>

/* CSS */
img {
    width: 100%;
    max-width: 100%;
    height: auto;
    display: block;
}

.box {
    width: 800px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

这个示例有以下几个特征:

  • 图片原始尺寸(1200 x 400)的宽度大于容器 .boxwidth
  • <img> 显示式设置的 widthheight 与原始图片尺寸不匹配(width=600height=300),且小于图片容器的宽度
  • CSS 的 img 设置了width: 100%; max-width: 100%; height: auto

它同要引起了布局偏移:

但当我们显式在 <img> 设置 widthheight 属性值与图片原始尺寸一致时,则不会造成 CLS:

<img src="https://picsum.photos/1200/400?random=1" alt="" width="1200" height="400">

或者说,你在编写的时候就知道图片的目标尺寸,那么在 <img> 中的 widthheight 设置为目标尺寸,不是图片原始尺寸,也不会造成 CLS:

<img src="https://picsum.photos/1200/400?random=1" alt="" width="800" height="267">

这种现象同样存在于:

  • 图片原始尺寸(1200 x 400)的宽度大于容器 .boxwidth
  • <img> 显示式设置的 widthheight 与原始图片尺寸不匹配(width=600height=300),且大于图片容器的宽度
  • CSS 的 img 设置了width: 100%; max-width: 100%; height: auto

示例:

<!-- HTML -->
<div class="box">
    <h2>我是图片</h2>
    <img src="https://picsum.photos/1200/400?random=1" alt="" width="800" height="400">
    <p>测试布局位移</p>
</div>

/* CSS */
img {
    width: 100%;
    max-width: 100%;
    height: auto;
    display: block;
}

.box {
    width: 400px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

实测试一下,只要 <img>widthheight 值不满足下面两个条件之一都会引起 CLS:

  • <img>widthheight 设置的值和图片原始尺寸不一致(偏大或偏小)
  • <img>widthheight 设置的值和图片容器值不一致

除此之外,我们可以像下面这样来避免图片设置的尺寸不一致而引起的 CLS,给 <img> 单独包裹一个容器,并且设置相应的 widthheight

<!-- HTML -->
<div class="box">
    <h2>我是图片</h2>
    <div class="figre">
        <img src="https://picsum.photos/1200/400?random=1" alt="" width="600" height="400">

    </div>
    <p>测试布局位移</p>
</div>

/* CSS */
.box {
    width: 300px;

    border: 1px solid #ccc;
    border-radius: 5px;
}

.figre { /* width : 100%, 即 300px */
    height: 100px;
    box-shadow: inset 0 0 0 2px red;
}

img {
    width: 100%;
    max-width: 100%;
    height: auto;
    display: block;
}

如果你不希望改变 DOM 结构,还可以使用 aspect-ratio 来避免 CLS:

<!-- HTML -->
<div class="box">
    <h2>我是图片</h2>
    <img src="https://picsum.photos/1200/400?random=1" alt="" width="600" height="400">
    <p>测试布局位移</p>
</div>

/* CSS */

.box {
    width: 800px;

    border: 1px solid #ccc;
    border-radius: 5px;
}

img {
    width: 100%;
    max-width: 100%;
    height: auto;
    display: block;
    aspect-ratio: 600 / 400;
}

如果 aspect-ratioattr() 结合得到浏览器支持的时候(即,attr() 函数获取的值可以用于 aspect-ratio),避免图片引起 CLS 的最佳解决方案是:

img {
    width: 100%; 
    height: auto; 
    aspect-ratio: attr(width) / attr(height); 
} 

在还没有得到浏览器支持情况下,建议手工 <img>widthheight 替换 aspect-ratio 中的 attr(width)attr(height)。这样可以完美的帮助我们解决图片引起的 CLS。

这里存在另一个值得探索和深究的话题。当图片原始尺寸小于图片容器尺寸时,CSS 的 widthmax-width (或 heightmax-height)设置为 100% 时,最终的渲染结果是不同的;max-width (或 max-height) 为 100% 计算出来的图片宽度(或高度)会小于图片容器宽度(或高度)!

使用响应式图片技术提升 LCP 的速度

图片有可能是影响 LCP 速度的元凶之一。为此,除了不使用图片、压缩图片,使用图片 CDN 等技术可以提升 LCP 速度之外,还可以使用响应式图片技术来提升 LCP 的速度。

我们先来看响应式图片相关的技术。

响应式图片

Web 开发者是随着 响应式设计的出现,才慢慢放弃在 元素中显式设置 width 和 height 属性,这样做的主要原因是,在处理不同终端的图片适配时,总是令人头痛的:

<img> 元素上显式设置 widthheight 属性值时,在不同终端的适配上总是会有一些问题:

<div class="box">
    <h2>我是图片</h2>
    <img src="https://picsum.photos/1200/400?random=1" alt="" width="600" height="400">
    <p>测试布局位移</p>
</div>

/* CSS */
.box {
    width: 80vw;
}

img {
    max-width: 100%;
    height: auto;
}

虽然可以在 img 中加上 width: 100% 来避免上面示例中图片无法填满容器的现象:

img {
    max-width: 100%;
    width: 100%;
    height: auto;
}

这样做很有可能会让图片在一定的情况下变得模糊,毕竟会有小图填充在大容器中。

随着技术的革新,有了更多的方案可以让我们在响应式布局中处理图片的适配,比如,使用 <img>srcsetsizes 新属性,根据不同的分辨率加载不同尺寸的图片源。有了 srcsetsizes 属性之后,可以像下面这样加载图片源:

这样浏览器就会选择最合适的图片。我们就能够提供更小或更大的图片。

srcsetsizes 属性看起来很复杂,其实没有想象的那么复杂。只要理解了 srcsetsizes 的含义,使用起来就会变得很容易:

  • srcset 定义了允许浏览器选择的图片集,以及每张图片的大小
  • sizes 定义了一组媒体条件并且指明当某些条件为真时,什么样的图片尺寸是最佳选择

通过下面几个小示例来阐述 srcsetsizes 的使用。

<img 
    src="https://picsum.photos/400/300?image=1068" 
    alt=" " 
    srcset="
        https://picsum.photos/400/300?image=1068 1x, 
        https://picsum.photos/800/600?image=1069 2x,
        https://picsum.photos/1200/900?image=1070 3x" 
/>

示例中的 srcset 根据设备的 dpr 来加载不同的图片源:

(dpr=1 时加载的是 https://picsum.photos/400/300?image=1068图片)

drp=2 时加载的是 https://picsum.photos/800/600?image=1069图片)

dpr=3 时加载https://picsum.photos/1200/900?image=1070图片 )

不支持 srcset 属性的浏览器将会使用 src 的图片(https://picsum.photos/400/300?image=1068)。

srcset 属性常配合 w 使用,比如:

<img 
    src="https://picsum.photos/320?image=1068" 
    alt="srcset for image" 
    srcset="
        https://picsum.photos/320?image=1068 320w, 
        https://picsum.photos/640?image=1069 640w,
        https://picsum.photos/960?image=1070 960w,
        https://picsum.photos/1280?image=1071 1280w,
        https://picsum.photos/1920?image=1072 1920w,
        https://picsum.photos/2560?image=1073 2560w
        " 
/>

上面示例中的 320w640w960w1280w1920w2560w 对应的是图片的实际宽度(像素单位),建议不要随意修改图片的尺寸。

除此之外, srcset 属性使用 w 宽度描述符时,必须配合 sizes 一起使用。这样可以覆盖更多的场景。sizes 属性通过描述图片最终呈现的宽度来告诉浏览器它需要多少像素。我们可以把 sizes 看作是一种提前向浏览器提供页面布局信息的方式,浏览器就可以在解析或渲染页面的任何 CSS 之前选择一个源。比如,我们在上面的示例上添加一个 sizes

<img 
    src="https://picsum.photos/320?image=10" 
    alt="srcset for image" 
    sizes="480px" 
    srcset="
        https://picsum.photos/320?image=68 320w, 
        https://picsum.photos/640?image=106 640w,
        https://picsum.photos/960?image=100 960w,
        https://picsum.photos/1280?image=9 1280w,
        https://picsum.photos/1920?image=22 1920w,
        https://picsum.photos/2560?image=33 2560w
        " 
/>

sizes 指定图片的尺寸宽度是 480px ,同时告诉浏览器图片将要显示成 480px ,而 srcset 设置图片的临界值:

  • [0, 320px] 对应 srcset 中的 320w
  • [320px, 640px] 对应 srcset 中的 640w
  • [640px, 960px] 对应 srcset 中的 960w
  • [960px, 1280px] 对应 srcset 中的 1280w
  • [1280px, 1920px] 对应 srcset 中的 1920w
  • [1920px, 2560px] 对应 srcset 中的 2560w
  • [2560px, ∞]

示例中的 sizes=480px 落在了 [320px, 640px] 这个区间,取最大值 640px ,因此 <img> 最终选择的是"https://picsum.photos/640?image=106":

这个时候,不管屏幕分辨率怎么变化,始终加载的是这张图片:

但设备 DPR 不同时,sizes=480px 对应的值就会不同,那么所取的图片源也将不同:

  • DPR = 1sizes=480px 对应的是 [320px, 640px] ,取 640px ,图片源是 “https://picsum.photos/640?image=106”,因为 480px x 1 = 480px
  • DPR = 2sizes=480 对应的是 [640px, 960px]960px,图片源是 “https://picsum.photos/960?image=100”,因为 480px x 2 = 960px
  • DPR = 3sizes=480px 对应的是 [960px, 1280px]1280px ,图片源是“https://picsum.photos/1280?image=9”,因为 480px x 3 = 1440px

sizes 属性值的单位可以是绝对单位,比如 px ,也可以是相对单位,比如 rem ,也可以是视窗单位,比如 vw 。在我们的业务开发时,时常使用 vw 单位,主要出发点是更好的适配不同的图终端。假设一个横幅广告图片,我们总是期望它的宽度是屏幕宽度的 100% ,这个时候可以设置 sizes 属性的值为 100vw:

<img 
    src="https://picsum.photos/320?image=10" 
    alt="srcset for image" 
    sizes="100vw" 
    srcset="
        tps-320-320.jpg 320w, 
        tps-640-640.jpg 640w,
        tps-960-960.jpg 960w,
        tps-1280-1280.jpg 1280w,
        tps-1920-1920.jpg 1920w,
        tps-2560-2560.jpg 2560w
        " 
/>

图片源会随着屏幕尺寸改变而改变,因为 sizes 属性单位是 vw ,屏幕宽度变了,sizes 的值也变了:

(上面是 DPR = 1时,屏幕尺寸改变,图片源也会改变)

(上面是 DPR = 2 时,屏幕尺寸改变,图片源也会改变)

同样的,sizes 属性值为 vw 或其他相对单位时,也和 px 类似(客户端最终计算出来的值单位为px),受设备 DPR 的影响。

sizes 属性除了可以简单指定图片在浏览器需要呈现的尺寸之外,还可以在此基础上添加媒体查询,例如:

<img 
    src="https://picsum.photos/320?image=10" 
    alt="srcset for image" 
    sizes="
        (min-width: 36em) 33.3vw,
        100vw
        " 
    srcset="
        tps-320-320.jpg 320w, 
        tps-640-640.jpg 640w,
        tps-960-960.jpg 960w,
        tps-1280-1280.jpg 1280w,
        tps-1920-1920.jpg 1920w,
        tps-2560-2560.jpg 2560w
        " 
/>

意思是,当视窗宽度小于 36em 时,图片在浏览器呈现区域的尺寸就会改变。即当屏幕尺寸在这个断点以下,图片的 sizes 的值是 100vw,否则是 33.3vw

可以按下面这个格式,在不同断点给 sizes 指定不同的值:

sizes = "
    [media query] [length],
    [media query] [length],
    [etc...],
    [default length]
"

浏览器会遍历 sizes 指定的媒体查询,当条件为真时,会匹配对应的长度值。如果没有匹配的媒体查询,那么浏览器会使用“默认”长度。

有了 sizes 和一组 srcset 中带有 w 描述符的图片源可供选择,浏览器就拥有了在流畅、响应式布局中有效加载图片所需的一切。另外,srcset 中的 wsizes 也给浏览器提供了足够的信息,使图片适应不同的设备像素比(device-pixel-ratios)。客户端会把 CSS 长度转换为 CSS 像素;然后乘以用户的设备像素比,浏览器就知道它需要填充的设备像素。

虽然我们的设备像素比用只适用于固定宽度的图片,并且只涵盖了 1x2x3x 屏幕,但这个 srcsetsizes 用例不仅涵盖了流体图片用例,而且还适用于任意的屏幕密度。

前面说过,<img>widthheight 是很重要的,那么在响应式图片中,它们同样重要。虽然, srcset 定义了允许浏览器选择的图片以及每张图片的大小,但为了保证 <img> 的宽度和高度属性可以进行设置,每张图片都应该采用相同的宽高比。

<img
    width="1000"
    height="1000"
    src="puppy-1000.jpg"
    srcset="puppy-1000.jpg 1000w, puppy-2000.jpg 2000w, puppy-3000.jpg 3000w"
    alt="小狗与气球"
    />

另外, <img>srcsetsizes 属性同样也适用于 HTML5 的 <picture> 子元素 <source>

<picture> 除了可以指定不同图片源和尺寸之外,还可以提供不同的图片格式:

<picture>
    <source
        type="image/avif"
        media="(-webkit-min-device-pixel-ratio: 1.5)"
        srcset="2x-800.avif 800w, 2x-1200.avif 1200w, 2x-1598.avif 1598w"
        sizes="
        (min-width: 1066px) 743px,
        (min-width: 800px) calc(75vw - 57px),
        100vw
        "
    />
    <source
        type="image/webp"
        media="(-webkit-min-device-pixel-ratio: 1.5)"
        srcset="2x-800.webp 800w, 2x-1200.webp 1200w, 2x-1598.webp 1598w"
        sizes="
        (min-width: 1066px) 743px,
        (min-width: 800px) calc(75vw - 57px),
        100vw
        "
    />
    <source
        media="(-webkit-min-device-pixel-ratio: 1.5)"
        srcset="2x-800.jpg 800w, 2x-1200.jpg 1200w, 2x-1598.jpg 1598w"
        sizes="
        (min-width: 1066px) 743px,
        (min-width: 800px) calc(75vw - 57px),
        100vw
        "
    />
    <source type="image/avif" srcset="1x-743.avif" />
    <source type="image/webp" srcset="1x-743.webp" />
    <img src="1x-743.jpg" width="743" height="477" alt="A red panda" />
</picture>

这里提到的响应式图片相关的技术都在 《Use Case and Requirements for Standardizing Responsive Image》中提供了相应的示例。

Responsive Image Breakpoints Generator可以快速帮助我们生成不同的断点下的图片:

艺术指导

艺术指导(Art Direction) ,也常被称为 “美术设计”,该概念源于响应式设计中的图片适配处理,即 当你想为不同布局提供不同剪裁的图片。比如在桌面布局上显示完整的、横向图片,而在手机布局上显示一张剪裁过的、突出重点的纵向图片。可以使用 HTML5 的 <picture> 元素来实现。

<picture> 除了能提供不同的尺寸的图片之外,还有另一个特性,可以提供不同格式的图片类型。换句话说,<picture> 同时具备提升 LCP 速度的两条优化手段:

  • 使用响应式图片,类似于 <img>srcsetsizes 特性,可以按需加载图片,减少图片尺寸,图片文件大小,减少加载时间
  • 使用不同格式图片,可以使用一些现代的图片格式,如 JPEG 2000,WebP,HEIC,AVIF 和 JPEG XL等

比如:

<picture>
    <source srcset="puppy.jxl" type="image/jxl">
    <source srcset="puppy.avif" type="image/avif">
    <source srcset="puppy.webp" type="image/webp">
    <source srcset="puppy.jpg" type="image/jpeg">
    <img src="puppy.jpg" alt="Cute puppy">
</picture>

在这个示例中,浏览器将开始解析源,当它找到第一个支持的匹配时就会停止。如果没有找到匹配的源,浏览器会加载 <img> 中指定的源作为备用源。这种方法可以很好地服务于并非所有浏览都支持的现代图片格式。千万要注意 <source> 元素的顺序,因为这个顺序非常重要。不要把现代图片格式的源放在传统格式之后,要把它们放在前面。简单地说,越先进,越还未得众多浏览器支持的图片格式越要放在最前面;越传统的越应该放在末位。得到浏览器支持的,浏览器就会选这个,未得到浏览器支持的,浏览器会自动忽略,并且开始往下寻找匹配的图片源。

时至今日,能用于 Web 上的图片格式也是种类繁多,要理解所有的图片格式是一个令人感到困惑的过程。不同的图片格式采用着不同的图片解码器,而图片解码器对于大多数 Web 开发者而言都是件不易的事,至少我就是这样的,我只知道图片的格式,但并不知道它对应的图片解码是怎么一回事。正如 JPEG 委员会中 JPEG XL 特设小组的主席 @Jon Sneyers 所说

一场“图片解码器之战” 就要开始了!

(上图来自:https://cloudinary.com/blog/time_for_next_gen_codecs_to_dethrone_jpeg)

目前已经有六种图片格式参与这场图片解码之战:

  • JPEG 集团的 JPEG 2000,它是 JPEG 的继任者,在Safari 浏览中可用
  • Google 的 WebP,可用于所有主流浏览器
  • MPEG 组织的 HEIC,基于 HEVC,在 iOS中可用
  • 开放媒体联盟(AOM)的 AVIF,在 Chrome 和 Firefox 浏览器中可用
  • JPEG XL,由 JPEG 组织开发,是下一代图片解码器
  • WebP2 是 WebP的实验性继任者

简单地说一下这种几种图片解码器:

  • JPEG 是为照片的有损压缩而创建的;PNG 是为无损压缩而创建的,它在非摄影图片上的表现最好。在某种程度上,这两种图片解码器是互补的,对于各种使用情况和图片类型,都需要它们
  • JPEG 2000 不仅优于 JPEG,而且还可以进行无损压缩。然而,对于非摄影图片,它落后于 PNG
  • WebP在压缩上以微弱的优势胜于 JPEG 和 PNG。对于高保真、有损压缩,WebP有时表现得比 JPEG 更差
  • HEIC 和 AVIF 处理照片的有损压缩要比 JPEG 有效得多。偶尔,它们在无损压缩方面落后于 PNG,但在处理有损的非摄影图片时却能产更好的效果
  • JPEG XL 在压缩效果上要比 JPEG 和 PNG 都要好,而且是跳跃式的

当有损压缩足够好的时候,例如,对于 Web 图片,AVIF 和 JPEG XL 都能提供比现有的 Web 图片解码器,包括 WebP 更好的结果。作为一项规则:AVIF在低保真、高保真的压缩中处于领先地位,而 JPEG XL 在中高保真中表现出色

回到我们平时开发流程中来,在开发的过程程,或许很关会关注不同图片类型的解码(算法),只是从设计软件中导出想要的图片格式,比如 Sketch设计软件:

然后将导出来的图片上传一 CDN:

在这个过程中,开发者可选的选项有限,其他的一切都交给了设计软件和 CDN 服务。当然,可能也有同学会使用 ImageOptim对图片进行压缩:

其实,在使用 Lighthouse 帮我们审查 Web 页面性能时,也会把不符合的图片格式提取出来,强调提供一下代格式图片可以带来的节约:

也可以使用图片审查工具,比如 Cloudinary 的图片分析工具,可以深入了解页面上所有图片的压缩建议。使用 WebPageTest 对页面分析时,其中分析报告中有一个 “Image Analysis” 选项,用的就是 Cloudinary 的图片分析工具:

点击“Image Analysis” 选项,会在新的窗口中显示页面中所有图片的压缩建议。或者直接进入 Webspeedtest.cloudinary.com 输入要审查图片的网址,也可以得到相应的结果:

也可以使用在线的 Squoosh 图片压缩工(也有支持 CLI,详见 Github),它支持一些最新的图片解码器,比如 JPEG XL,WebP2,AVIF等:

使用 Squoosh 做一个测试:

(测试原图)

压缩前后的对比:

有了这些不同格式的图片之后,就可以使用 <picture> 来提供多种格式的图片,让浏览器自己做出最佳选择:

<picture>
    <source type="image/jxl" srcset="optimization-jxl.jxl" />
    <source type="image/wp2" srcset="optimization-wp2.wp2" />
    <source type="image/avif" srcset="optimization-avif.avif" />
    <source type="image/webp" srcset="optimization-webp.webp" />
    <img src="optimization-png.png" width="536" height="111" alt=" " />
</picture>

使用开发者工具,Chrome 首先加载的是 AVIF格式的图片:

如果禁用 AVIF 格式,就会加载 WebP格式图片:

当这些新型图片格式都不支持的浏览器,就会加载 <picture><img> 提供的备用图片:

<picture> 除了使用 <source> (指定 type 类型)来加载不同格式图片之外,也可以在 <source> 上使用 srcsetsizes 来加载不同尺寸的图片。

<picture>
    <source
        type="image/avif"
        media="(-webkit-min-device-pixel-ratio: 1.5)"
        srcset="2x-800.avif 800w, 2x-1200.avif 1200w, 2x-1598.avif 1598w"
        sizes="
        (min-width: 1066px) 743px,
        (min-width: 800px) calc(75vw - 57px),
        100vw
        "
    />
    <source
        type="image/webp"
        media="(-webkit-min-device-pixel-ratio: 1.5)"
        srcset="2x-800.webp 800w, 2x-1200.webp 1200w, 2x-1598.webp 1598w"
        sizes="
        (min-width: 1066px) 743px,
        (min-width: 800px) calc(75vw - 57px),
        100vw
        "
    />
    <source
        media="(-webkit-min-device-pixel-ratio: 1.5)"
        srcset="2x-800.jpg 800w, 2x-1200.jpg 1200w, 2x-1598.jpg 1598w"
        sizes="
        (min-width: 1066px) 743px,
        (min-width: 800px) calc(75vw - 57px),
        100vw
        "
    />
    <source type="image/avif" srcset="1x-743.avif" />
    <source type="image/webp" srcset="1x-743.webp" />
    <img src="1x-743.jpg" width="743" height="477" alt="A red panda" />
</picture>

<srouce> 上使用 srcsetsizes 的方法和 在 <img> 上使用 srcsetsizes 方法是等同的。具体使用可以根据自己业务场景和用户范围的做出判断:

如果不想考虑过多场景,可以针对用户设备的 DPR 做判断,加载不同大小的图片:

<picture>
    <source type="image/jxl" srcset="
                                    optimization-jxl@1x.jxl 1x,
                                    optimization-jxl@2x.jxl 2x,
                                    optimization-jxl@3x.jxl 3x" />
    <source type="image/wp2" srcset="
                                    optimization-wp2@1x.wp2 1x,
                                    optimization-wp2@2x.wp2 2x,
                                    optimization-wp2@3x.wp2 3x" />
    <source type="image/avif" srcset="
                                        optimization-avif@1x.avif 1x,
                                        optimization-avif@2x.avif 2x,
                                        optimization-avif@3x.avif 3x" />
    <source type="image/webp" srcset="
                                        optimization-webp@1x.webp 1x,
                                        optimization-avif@2x.webp 2x,
                                        optimization-avif@3x.webp 3x" />
    <img 
        src="optimization-png.png" 
        width="536" 
        height="111" 
        alt=" " 
        srcset="
                optimization-png@1x.png 1x,
                optimization-png@2x.png 2x,
                optimization-png@3x.png 3x
                "
        />
</picture>

CSS 中吶应式图片处理

在 Web 中除了 <img> (结合 srcsetsizes 属性)或 <picture> 可以将图片添加到 Web 中。除此之外,在 CSS 中可以使用 <image> 数据类型的 CSS 属性也可以将图片添加到 Web 上,比如 background-imagemask-imageborder-imagelist-style-image 等。如果希望在 CSS 中的使用的图片也能像 <img><picture> 那样,按需加载图片源,应该怎么做呢?

其中最简单的方式,就是使用 CSS 媒体查询,比如:

.img {
    background-image: url(small.jpg);
}
@media (min-width: 468px),
    (-webkit-min-device-pixel-ratio: 2), 
    (min-resolution: 192dpi) {
    .img {
        background-image: url(large.jpg);
    }
}

注意,mask-imageborder-imagelist-style-image 也可以像上面那样使用。另外,媒体查询的条件,你可以根据你的具体使用场景来决定。

如果只是希望在 background-image 实现按需加载图片源的话,我们还可以使用 image-set() 函数,实现类似于 <img>srcset 部分功能,比如根据 DPR 值加载不同的图片源:

.hero {
    /* 不支持 image-set() 浏览器会使用该图片 */
    background-image: url("platypus.png");

    background-image: image-set(
        "platypus.png" 1x, 
        "platypus-2x.png" 2x,
        "platypus-3x.png" 3x
    );
}

而且 image-set() 函数还可以实现类似 <picture><source> 的功能,加载不同的图片格式:

.div2 {
    background-image: url(puppy.png);
    background-image: image-set( 
        "puppy.webp" type("image/webp") 1x,
        "puppy2x.webp" type("image/webp") 2x,
        "puppy.png" type("image/png") 1x,
        "puppy2x.png" type("image/png") 2x
    );
}

图片懒加载

在用户向下滚动页面之前,屏幕外的图片是不可见的,对于这些不可见的图片,我们可以在 <img>loading 属性来控制它们加载的行为:

  • loading="lazy" 懒加载图片,将它们的加载推迟到与视口的计算距离
  • loading="eager" 立即加载图片,不管图片是否在视口区。

eagerloading 的默认值 。来看一个简单的示例:

<img src="donut.jpg"
    alt="A delicious pink donut."
    loading="lazy"
    width="400"
    height="400"
    >

理想情况,首屏之外的图片都应该是懒加载(即loading="lazy"),首屏内的图片则避懒加载。但也并不代表首屏的所有图片都应该是立即加载。比如下面这样的场景,页面氛围图随着轮翻图变化,可能会用到多张图片,那么在首异加载渲染时,并不代表着所有氛围图都应该立即加载:

loading 也适用于设置了 srcset<img>

<img src="donut-800w.jpg"
    alt="A delicious donut"
    width="400"
    height="400"
    srcset="donut-400w.jpg 400w,
            donut-800w.jpg 800w"
    sizes="(max-width: 640px) 400px,
            800px"
    loading="lazy">

除了对 srcset 起作用之外,loading 属性还对 <picture> 里的 <img> 起作用:

<picture>
    <source media="(min-width: 40em)" srcset="big.jpg 1x, big-hd.jpg 2x">
    <source srcset="small.jpg 1x, small-hd.jpg 2x">
    <img src="fallback.jpg" loading="lazy">
</picture>

Lighthouse 的优化建议也会列出页面上任何可以被懒加载的图片源:

预加载图片

帮助浏览器尽早发现影响 LCP 速度的图片,以便它能以最小的延迟获取并渲染它。在可能的情况下,尝试通过更好的减少对你的 LCP 图片的请求链来解决这个问题,这样浏览器就不需要首先获取、解析和执行 JavaScript,或者等待组件渲染(Render)/合成(Hydrate) 来发现图片。

正如 Lighthouse 的优化建议,可以使用 <link rel="preload"><img> 一起使用,让浏览器在 HTML 中发现你要加载的关键资源之前,尽快发现它们:

<head>
    <link rel="preload" as="image" href="6000000002031-2-tps-1125-951.png_790x10000.jpg_.webp"/>
</head>  
<body>
    <img src="6000000002031-2-tps-1125-951.png_790x10000.jpg_.webp" width="790" height="668" >
</body>  

Lighthouse 优化建议的列表中除了建议预加载影响 LCP 速度的图片资源之外,还建议对关键请求应该做预加载:

如果你正在优化 LCP,预加载可以帮助提高后期发现的图片(例如那些由 JavaScript 加载的图片或CSS中的背景图)被获取的速度。如果你需要关键图片(如聚划算首页的氛围图)优先于页面上其他图片的加载,那么预加载可以带有来意义的变化。

注意,使用预加载了是有风险的,需要谨慎地使用,并始终在生产中衡量其影响。如果你的图片的预加载在文档中比它早,这可以帮助浏览器发现它(并相对于其他资源排序)。如果使用不当,预加载可能会导致你的图片延迟 LCP(比如 CSS、字体),得到效果正好相反。还要注意,要使用这种重新安排优先次序加载资源有效,还要取决于服务器正确安排请求的优先次序

预加载(preload)可以与响应图片一起使用,将两种模式相结合能够实现更快速图片加载:

<head>
    <link 
            rel="preload" 
            as="image" 
            href="wolf.jpg" 
            imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" 
            imagesizes="50vw" 
        />
</head>
<body>
    <img 
        src="wolf.jpg" 
        srcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" 
        sizes="50vw" 
        alt="A rad wolf" 
        />
</body>  

(图片是否使用预加载的瀑布图对比,左侧未使用,右侧使用)

不幸的是,到目前为止 imagesrcsetimagesizes 只适用于带有 srcsetsizes<img> 元素,并不适用于 <picture> 以及其内部的 <img> 元素。如果的确需要在 <picture> 中的图片源做预加载,可以像下面这样变通:

<head>
    <link rel="preload" href="small_cat.jpg" as="image" media="(max-width: 400px)">
        <link rel="preload" href="medium_cat.jpg" as="image" media="(min-width: 400.1px) and (max-width: 800px)">
        <link rel="preload" href="large_cat.jpg" as="image" media="(min-width: 800.1px)">
</head>  
<body>
    <picture>
        <source src="small_cat.jpg" media="(max-width: 400px)">
        <source src="medium_cat.jpg" media="(max-width: 800px)">
        <img src="huge_cat.jpg">
    </picture>
</body>  

图片解码

浏览器需要对其下载的图片进行解码,以便将它们变成你屏幕上的像素。然而,浏览器处理延迟图片的方式可能不同。比如,Chrome 和 Safari 浏览器尽可能的将图片和文本一起渲染,即 同步渲染。这在视觉上看起来是对的,但图片必须解码,这可能意味着在完成图片解码之前,文本有可能不会被渲染。<img> 元素上的 decoding 允许你控制浏览器对图片解码是 同步 还是 异步。

<img src="donut-800w.jpg"
    alt="A delicious donut"
    width="400"
    height="400"
    srcset="donut-400w.jpg 400w,
            donut-800w.jpg 800w"
    sizes="(max-width: 640px) 400px,
            800px"
    loading="lazy"
    decoding="async">

<img> 上显式设置 decoding="async" 可以让浏览器在主线程之外对图片进行解码, 避免用户在图片解码时受到 CPU 时间的影响。decoding 还有别外两个值:

  • sync 提示浏览器图片解码不应该推迟
  • auto 提示浏览器做它认为最好的事情

默认情况下,图片的解码不是 异步 (不阻断光栅)。如果这样的话,Web 上有太多的内容会在加载时出现闪烁。然而,所有的解码都是脱离主线程的。

图片占位符

如果你想在图片加载时向用户显示一个占位符(如,模糊的点位符),可以在 <img> 元素上使用 background-imagebackground-size: cover 结合起来,设置一个元素的大小的背景图片,并在不拉伸图片的情况下尽可能地扩大图片。

图片占位符通常是内联的、Base64 编码的数据 URL,它是低质量图片占位符(LQIP)或 SVG 图片占位符(SQIP)。这允许用户在加载更清晰的最终图片之前,获得非常快的图片预览,即使在缓慢的网格连接上也是如此:

<img src="donut-800w.jpg"
    alt="A delicious donut"
    width="400"
    height="400"
    srcset="donut-400w.jpg 400w,
            donut-800w.jpg 800w"
    sizes="(max-width: 640px) 400px,
            800px"
    loading="lazy"
    decoding="async"
    style="background-size: cover; 
            background-image:
            url(data:image/svg+xml;base64,[svg text]);">

比如这个示例,在下载全尺寸图片之前,用户会立刻看到一个模糊的图片效果:

正如上面录屏所示,在加载图片时显示一个近似于最终图像占位符。这在某些情况下可以提高感性能。

对于图像占位符有多种现代解决方案(比如 CSS 背景色、LQIP、SQIP、Blur Hash和Potrace)。哪种方法对你的用户体验最有意义,可能取决于你在多大程度上试图提供最终内容的预览、显示进度(如渐进式加载)或只是避免在图片最终加载时出现视觉闪光(Visual Flash)。比如下面这个,模糊和渐变的占位符效果差异:

JPEG XL 支持的完全的渐进式渲染也是很好的一种选择。下图是来自 @CyberAgent的 Gunther Brunner的感知性(Perceptual)图片加载方法

最后,再提供 <img> 中使用图片占位符实现相关的说明:

  • 将模糊的占位符内联为 <img>background-image (背景图片)。这样避免了使用额外的 HTML 元素,并且在图片加载时自然隐藏图片占位符,因此也不需要任何 JavaScript 脚本来辅助
  • 将实际图片的数据 URI 放在一个 SVG 图片的数据 URI 中。这样做是因为图片的模糊处理是在 SVG 中完成的,而不是CSS 中滤镜。如此一来,当 SVG 被光栅化时,每张图片只执行一次模糊处理,而不是在每个布局上执行,这样可以节省 CPU
  • 占位符使用内联数据 URL,在初始化 HTML 中就能提供,避免了额外的网格请求,建议占位符图片的大小 ≤ 1 ~ 2 KB是最佳的。
  • LCP 会考虑到占位符图片的内在尺寸,所以最好是让“预览”与正在加载的真实图片的内在尺寸保持一致

延迟离屏内容的渲染

CSS 的 content-visibility 属性允许浏览器跳过元素的 渲染、布局 和 绘制 ,直到它们被需要时才渲染。简单地说,它可以延迟屏幕外的内容(离屏幕内容)渲染时间。如果你的页面有大量的内容在屏幕外,包括使用 <img> 元素的内容,那么使用 content-visibility: auto 可以帮助你优化页同加载性能。content-visibility: auto 可以减少浏览器 CPU 工作, 以减少前期的工作,包括屏幕外的图片解码:

img {
    content-visibility: auto;
}

/* section 区域中 img 在屏幕外时生效,即只对 section 可滚动区域内的图片才有意义 */
section img{
    content-visibility: auto;
}

content-visibility 可以取 visibleautohidden 三个值,其中 visible 是其默认值, 不过,只有取值为 auto 才能帮你优化页面性能。页面中使用 content-visibility: auto 获得对布局、绘制和样式的遏制。如果该元素不在屏幕上,它也会得到尺寸遏制。

当用 content-visibility:auto 将一个页面分块时,开发者观察到渲染时间因此有了 7 ~ 10 倍的改善。对于一个长的 HTML 文档,渲染时间从 360ms 降到了 35ms

浏览器不会为受content-visibility 影响的图片绘制图片内容,所以这种方法可能会带来一些优化。

section {
    content-visibility: auto;
    contain-intrinsic-size: 700px; 
}

如果它受到尺寸的遏制的影响,你可以将 content-visibilitycontain-intrinsic-size 一起使用,后者提供了元素的自然尺寸。比如上面代码为每个 section 元素提供近似 700px 的宽度和高度。

小结

在开发中使用图片时,我们应该时刻注意以下这些小细节,因为这些小细节可以让我们的页面给用户一个更好的体验:

  • 别忘了 <img> 中的 alt 属性,给依赖 ATs技术的用户提供相应的图片描述信息
  • 如果有描述文本需要向用户显示时,应该把 <img> 放在 <figure> 中,并且给 <img> 设置 aria-hidden="true,把文本信息放置在 <figcaption> 中,这样 ATs 技术会向用户呈现 <figcaption> 提供的描述信息
  • 使用 像 Squoosh这样的图片压缩工具对图片进行压缩
  • 别忘了 <img>widthheight 属性,尽可能的将 <img>widthheight 属性值与图片原始宽高相匹配。显示设置图片宽高可以有效的避开 CLS的触发
  • 在CSS中显示指定 imgaspect-ratio 值是 <img>widthheight 属性值比例,即 aspect-ratio: attr(width) / attr(height) ,在attr()函数还不能使用在 aspect-ratio 时,可以直接把 <img>widthheight 值运用于 aspect-ratio: width / height。这样更有效的避免 CLS 的触发
  • 设置 imgwidthmax-width 值为 100% 易于适配不同大小容器,但也有可能引起图片模糊;在某些场景使用 object-fit 来控制图片与容器适配也是一种较佳的选择
  • <img>srcsetsizes 可以同时指定不同尺寸的图片源,提示浏览器根据环境选择最佳的图片源,有利于 LCP 速度的提升
  • 即使 <img>sizes 可以告诉浏览器预呈现的尺寸大小,但也不要忘了该元素的 widthheight 设置
  • <picuter><source> 可以指定不同的图片格式,浏览器会选择最佳的图片格式,这也有利于 LCP 的提速
  • srcsetsizes 属性同样也适用于 <picture><source> 元素,可以为不同的图片格式提供不同的图片尺寸
  • 如果期望 CSS 中的图片也能根据不同的环境提供不同的图片源,除了使用 CSS 媒体查询之外,还可以使用 image-set() 函数,但该函数只适用于 background-image
  • 如果图片是一个 LCP 元素或首并渲染的关键元素,应该使用 preload 对图片进行预加载,但使用预加载时需要格外的小心
  • 如果图片不是 LCP 元素也不是渲染关键元素,应该在 <img> 中使用 loading="lazy" 对图片进行延迟加载
  • 对于离屏的图片,除了使用延迟加载之外,还应该使用 content-visibility: auto 让图片离屏渲染,可以减少主线程 CPU 的使用,提高页面渲染速度
  • 有效使用 <img>decoding 属性,告诉浏览器图片解码是异步(async)还是同步(sync),避免图片解码时受到 CPU时间的影响
  • <img> 元素上内联方式使用 background-size: coverbackground-image 使用图片占位符。图片占位符建议采用 LQIP、SQIP 或者直接使用 CSS 渐变(可以使用 Placeholder 工具在线生成),建议占位符图片应该小于 2kb。LCP 会考虑到占位符图片的内在尺寸,所以最好是让”预览“与正在加载的真实图片的内在尺寸保持一致。
  • 使用图片占位符可以给用户提供一种感知性的加载体验之外,同样也是优化 LCP 的一种技术手段

上面列出的仅仅是图片优化(或使用)的一些小细节,但这些小细节对于 LCP 和 CLS 的优化有着显著的效果。开篇就说过,图片是 Web 上的重要信息传递元素,是Web上不可或缺的元素,并且对于现代 Web 页面或应用(特别是 C端)图片的使用更多,占用的资源也更大。同时也是影响用户体验的最要元凶之一。也就是说,如果我们把图片用好了,优化好了,对于Web的性能和用户体验都会有显著提高。

如果文章中有阐述不对之处,还请各路大神拍正。

参考资料

返回顶部