響應式圖片 srcset:3 種寫法怎麼選

打開 Chrome 開發者工具,切到手機模擬視角,點開 Network 面板一看,手機下載的那張首圖,居然跟桌機版本是同一個檔案、同一個位元組數。CSS 把圖片顯示得比手機螢幕還窄,但瀏覽器一個位元組都沒少下載,白白耗掉的流量與載入時間,行動網路的使用者最先感受得到。

問題不在 CSS,而在 <img src="..."> 這種最基本的寫法只能指向單一個檔案,瀏覽器不會自己看裝置去換圖。這件事得靠 HTML 原生的響應式圖片(responsive images)機制,核心是 srcset 屬性,由它一次列出好幾種尺寸的圖片,交給瀏覽器自己判斷該下載哪一張,全程不用多寫一行 JavaScript。根據 web.dev 的說明,向行動裝置放送電腦版尺寸的圖片,可能會多耗費 2 到 4 倍不必要的資料量。

這篇把 srcsetsizespicture 三種寫法拆開講清楚,先從瀏覽器為什麼不會自己動手縮圖講起,再一路拆到三種寫法怎麼選、怎麼寫。

瀏覽器為什麼不會自己幫手機縮圖?

開頭那個 Network 面板的畫面,其實藏著一個很多人會誤解的地方,CSS 的 max-width: 100%width: 100%,只是把圖片顯示得比較小,瀏覽器該下載的檔案大小完全沒有改變。縮小的是畫面上看到的尺寸,不是實際傳輸的資料量,要真正省流量,得從圖片檔案本身下手,而不是靠 CSS 縮放。

HTML 規格把「該給手機什麼圖」這件事拆成兩個不同的問題。所謂的解析度切換(resolution switching),指的是同一張圖片,內容與裁切都一樣,只是螢幕的像素密度或顯示尺寸不同,需要換一個檔案大小;另一個問題是藝術指導(art direction),意思是螢幕小到某個程度之後,光換檔案大小已經不夠,乾脆換一張裁切或構圖不同的圖片才合理,例如把桌機版的橫幅裁成手機版的直式特寫。這篇後面的做法一、做法二處理的是解析度切換,做法三才是要處理藝術指導,讀完這節,你就能判斷自己的圖片問題出在哪一種。

並排比較解析度切換與藝術指導:前者換同一張圖的不同檔案大小,後者換一張裁切與構圖都不同的圖
解析度切換只換同一張圖的檔案大小,藝術指導則連裁切、構圖都換成另一張圖。

不論解決哪一個問題,整套機制都寫在 HTML 屬性裡,瀏覽器讀到屬性就自己判斷該挑哪張圖,全程不需要另外寫 JavaScript 去偵測裝置寬度或像素密度。

三種寫法該怎麼選?

面對一張圖,與其一開始就鑽進語法細節,不如先問自己三個問題:

  1. 這張圖的顯示尺寸會不會隨螢幕寬度縮放?不會的話,通常是 logo、大頭貼、固定尺寸的功能圖示這類元素,只要處理解析度就好,用做法一的 x descriptor 最省事。
  2. 顯示尺寸會不會隨版面縮放?文章內文的滿版圖、卡片牆縮圖大多屬於這一類,要用做法二的 w descriptor 搭配 sizes 屬性。
  3. 連圖片的內容本身都要換嗎?包含裁切構圖不同,或想依瀏覽器支援度換成 AVIF、WebP 這類新格式,這種情況才需要做法三的 picture 元素。

三種寫法不是互斥的選擇題,還可以疊加使用,picture 元素裡的 source 標籤,本身也能再搭配 srcsetsizes,等於同時處理「換構圖」跟「換解析度」兩件事。下面就照這三種做法逐一拆開講。

用三個問題對應 srcset 加 x、srcset 加 sizes、picture 三種響應式圖片寫法,判斷該用哪一種
顯示尺寸固定用 srcset+x、會隨版面縮放用 srcset+sizes、連圖片內容都要換才用 picture。

做法一:用 srcset 讓瀏覽器自動挑對解析度

做法一適用的情境很單純,圖片的顯示尺寸是固定的,只是不同裝置的螢幕解析度不一樣。常見的例子是導覽列的 logo、留言區的大頭貼,這些元素不會因為換到手機就變大變小,顯示尺寸永遠一樣,差別只在螢幕夠不夠細緻。

寫法是在 srcset 屬性裡,用逗號分隔列出好幾個檔案,並在每個檔案後面加上一個像素密度(pixel density)倍數,也就是 x descriptor:

<img
  src="logo.png"
  srcset="logo.png, logo-2x.png 2x, logo-3x.png 3x"
  alt="站台 logo">Code language: HTML, XML (xml)

桌機螢幕多半是 1x,一般的手機螢幕大多落在 2x、有些旗艦機甚至到 3x(所謂的 retina 螢幕),瀏覽器會依裝置實際的像素密度,自己挑出對應倍數的檔案下載,顯示尺寸不會改變,只有畫面清晰度會不一樣。

這裡有一個容易被忽略的細節,src 屬性不能拿掉。它同時身兼兩個角色,一是當作沒有特別標記倍數的 1x 版本,二是給不支援 srcset 的舊瀏覽器當保底(fallback)。只要瀏覽器認得出 srcset,就會優先套用它去挑檔案,src 純粹是保險。

適用情境:尺寸固定的圖示與大頭貼

導覽列的 logo、留言區的使用者大頭貼、固定 48 像素大小的功能圖示,都屬於這一類,尺寸幾乎不會因為版面而變動,唯一在意的是螢幕夠不夠細緻。跟下一節「會隨版面縮放」的內文圖不同,這類元素套用 x descriptor 就夠了,不需要再處理 sizes

做法二:srcset 加上 sizes,因應版面縮放的圖片

做法二要處理的情境跟做法一不同,圖片的顯示尺寸會隨螢幕寬度或版面改變,像文章內文的滿版圖、卡片牆的縮圖都是這一類。同一張圖,在手機上可能占滿整個寬度,到了桌機卻只占版面的一半或三分之一。

這時候 srcset 裡列的不再是像素密度倍數,而是每個檔案實際的像素寬度,用 w 為單位標記,例如 pic-480.jpg 480w。光有檔案寬度還不夠,還要搭配 sizes 屬性,告訴瀏覽器「在什麼版面條件下,這張圖大概會顯示多寬」:

<img
  src="hero-800.jpg"
  srcset="hero-480.jpg 480w, hero-800.jpg 800w, hero-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="文章配圖">Code language: HTML, XML (xml)

sizes 為什麼非給不可?因為瀏覽器讀取網頁的順序是先讀 HTML、再解析 CSS,如果 HTML 裡沒有 sizes 這個提示,瀏覽器在還沒讀到 CSS 之前,根本不知道這張圖片最後會顯示多寬,也就無從決定該下載 srcset 清單裡的哪一個檔案。sizes 本身不會改變圖片的顯示大小,那仍然是 CSS 的工作,它只是先告訴瀏覽器一個大概的版面寬度,讓瀏覽器提早做出下載的判斷。

瀏覽器讀 srcset 與 sizes 後的挑圖流程:讀 HTML、拿候選清單、看版面提示、估算顯示寬度,再挑一張下載
有了 sizes 當版面提示,瀏覽器才能在讀到 CSS 前就估出顯示寬度,從 srcset 清單挑對檔案。

這個屬性寫錯的後果也很直接,寫得比實際顯示尺寸小,瀏覽器會挑到解析度不夠的檔案,圖片放大後糊掉;寫得比實際顯示尺寸大,瀏覽器會下載超出需要的檔案,流量就白白浪費掉,等於繞了一圈又回到開頭那個問題。

sizes 這樣寫,才不會挑錯圖

sizes 不需要算到小數點,只要抓一個大概值就夠用,這張圖在各個斷點大概占螢幕寬度的幾成,寫出對應的百分比或 vw 單位即可。例如一張手機版滿版、桌機版占一半版面的圖片,可以寫成 sizes="(max-width: 600px) 100vw, 50vw"。這代表螢幕寬度在 600 像素以下,圖片顯示成整個版面寬,超過 600 像素之後就顯示成版面的一半。

要注意兩件事:

  1. 清單裡沒有加任何條件的最後一個值,就是預設值,也就是前面所有條件都不符合時才會用到的那個寬度。
  2. sizes 清單的順序要由窄到寬依序寫,順序寫反,瀏覽器會直接套用第一個符合的條件,後面的判斷就整組失效。

做法三:用 picture 元素換圖,不只是換尺寸

前面兩個做法換的都是同一張圖片的不同尺寸,做法三不一樣,適用情境是連圖片內容本身都要換。常見的兩種需求,一種是手機版想放裁切過、只聚焦主體的直式構圖,桌機版才用完整的橫幅;另一種是想依瀏覽器支援度,優先給新一代的 AVIF 或 WebP 格式,不支援的舊瀏覽器再退回熟悉的 JPG。

這時候要用的是 picture 元素,基本結構是外層包一個 picture,裡面放幾個 source 標籤,最後一定要留一個 img:

<picture>
  <source media="(max-width: 799px)" srcset="hero-mobile.jpg">
  <source media="(min-width: 800px)" srcset="hero-desktop.jpg">
  <img src="hero-desktop.jpg" alt="首頁主視覺">
</picture>Code language: HTML, XML (xml)

source 標籤的 media 屬性負責依版面條件切換不同構圖,也就是藝術指導要處理的問題,瀏覽器會由上而下,挑第一個符合條件的 source 使用。type 屬性則負責依格式支援度做 fallback,瀏覽器會挑自己認得的第一種格式下載,不認得就跳過、換下一個。

最後那個 img 元素千萬別省略,它是所有 source 條件都不成立時的保底顯示,也是不支援 picture 元素的舊瀏覽器唯一看得懂的版本,沒有它,萬一條件都不符合,圖片就整個顯示不出來。

media 和 type 可以一起用嗎?

想同時做出「大螢幕給 AVIF 裁切圖、小螢幕給 WebP 特寫圖」這種複合寫法,作法是把 mediatype 疊在同一組 source 標籤上:

<picture>
  <source media="(min-width: 800px)" type="image/avif" srcset="hero-desktop.avif">
  <source media="(min-width: 800px)" type="image/webp" srcset="hero-desktop.webp">
  <source media="(max-width: 799px)" type="image/avif" srcset="hero-mobile.avif">
  <source media="(max-width: 799px)" type="image/webp" srcset="hero-mobile.webp">
  <img src="hero-desktop.jpg" alt="首頁主視覺">
</picture>Code language: HTML, XML (xml)

只是要老實提醒,每多疊一個條件,source 標籤就會多一整組,程式碼量會明顯變長,上面這個例子光是兩種版面乘上兩種格式,就已經寫出四行 source。建議只在真的同時需要「換構圖」又「換格式」時才這樣疊加,多數情況下擇一使用,程式碼會單純很多。

srcset 之外,這 3 個屬性能再幫手機省流量

三種寫法都選對之後,還有三個常被漏掉、卻很吃效能分數的細節,一併補上:

  1. widthheight(或用 CSS 的 aspect-ratio 取代):讓瀏覽器在圖片檔案真正下載完成之前,就能先算好版面要留多大的空間,避免圖片載入時把周圍的文字內容一起往下推,造成畫面跳動。
  2. loading 屬性的分工:不在第一屏出現的圖片,加上 loading="lazy",可以延後到使用者捲動到附近才開始下載;首屏的重要圖片則相反,要維持預設的 loading="eager",重要到會影響使用者第一眼觀感的圖片(例如首頁主視覺),還可以再加 fetchpriority="high",提早搶下載順位。
  3. sizes="auto" 搭配 loading="lazy":這是這一兩年比較新的簡化寫法。有了它,瀏覽器會直接依圖片實際排版出來的尺寸,去 srcset 清單裡挑圖,不用再手動去算、去寫 sizes 的 media condition,等於把「算 sizes」這件最麻煩的事,交還給瀏覽器自己處理。
<img
  src="card-640.jpg"
  srcset="card-320.jpg 320w, card-640.jpg 640w, card-960.jpg 960w"
  sizes="auto"
  loading="lazy"
  width="640"
  height="360"
  alt="卡片牆縮圖">Code language: HTML, XML (xml)

目前 Chrome 與 Firefox 已經正式支援 sizes="auto",Safari 目前尚未支援。正因為 Safari 還沒跟上,保險的寫法是同時寫 sizes="auto, 100vw",讓看得懂 auto 的瀏覽器優先用它,看不懂的瀏覽器就直接退回後面那個固定值。這些全部都是瀏覽器原生就會做的事,呼應前面說過的,整套響應式圖片機制從頭到尾都不需要另外寫 JavaScript 去判斷裝置。

回到最開頭那個 Network 面板的畫面,把對應的 srcsetsizespicture(依這篇的判斷選一種)補上去之後,重新整理頁面,再切回手機模擬視角看一次 Network 面板,你會看到手機這次真的下載了比桌機小的檔案,不再是同一個位元組數。

sizes="auto" 這類新屬性,正在把「計算版面顯示尺寸」這件最麻煩的事,一步步交還給瀏覽器自己處理。往後要為圖片手動寫的判斷條件只會越來越少,這件事也只會變得越來越單純。

資料來源
  1. Serve responsive images — web.dev