· 8 分鐘閱讀

前端使用圖片時需要注意的事

# 前端

最近重新檢視自己 blog 的圖片,發現很多文章還是直接一個 <img src="..."> 就丟出去,沒有設 widthheight。在手機上開的時候會看到內容因為圖片載入往下跳。

這個現象叫做 Layout Shift,指的是畫面上非預期的位置偏移。常見的成因是圖片本身太大、或是網速跟不上,導致瀏覽器在不知道圖片尺寸的情況下先把版面排好,等圖片載入後才回頭調整。我也看過刻意製造 layout shift 來誘導使用者誤點廣告的網站,雖然這種設計有沒有「故意」見仁見智,但讀者體感就是不好。

剛好看到 Jake Archibald 寫過一篇文章整理過響應式圖片的最佳實踐,我順手把自己的想法跟做法整理一遍。額外一提,我很喜歡 Jake 主持的 HTTP203(是一個在 Chrome for Developers 上的節目),有很多關於瀏覽器與網頁的深度解析。

width 跟 height 為什麼一定要加

很多人會想說我用 CSS 都已經設定圖片寬度了,HTML 上的 width height 不就重複嗎?而且寫死數字感覺很不彈性,所以乾脆不寫。

<img src="hero.jpg" alt="hero" />

這樣寫的問題是:在圖片載入完成之前,瀏覽器根本不知道這張圖片多大,所以會先用 0 高度去 layout。等到圖片下載進度跑到一定程度、抓到圖片本身的尺寸資訊之後,再回頭把對應的空間騰出來。

於是你就會看到畫面突然往下跳一段。這就是所謂的 CLS(Cumulative Layout Shift),是 Web Vitals 裡面會直接影響 SEO 分數的指標之一。

正確的寫法是:

<img src="hero.jpg" width="1600" height="900" alt="hero" />

寫上 width="1600" height="900",瀏覽器一看到 HTML 就知道這張圖片的長寬比是 16:9,就算實際上 CSS 用的是 width: 100%,瀏覽器仍然可以根據比例算出對應的高度,預留好空間。等圖片載入完成,畫面就不會跳動。

HTML 上的 width height 不是真的尺寸,而是給瀏覽器算 aspect ratio 用的。你的 CSS 仍然會接管實際呈現的尺寸:

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

從 2020 年開始,Chrome、Firefox、Safari 都會根據 widthheight 屬性自動推導出 CSS 的 aspect-ratio1,相當於背後幫你寫了:

img[width][height] {
  aspect-ratio: attr(width) / attr(height);
}

所以你只要乖乖把 widthheight 填上,CSS 還是想怎麼縮放就怎麼縮放,CLS 同時也解決了。除非你的版面真的是動態決定、連比例都拿不到(後面會講這種情況怎麼處理),不然預設就應該把這兩個屬性加上去。(不過我還是常常偷懶

CSS 的 aspect-ratio 什麼時候用

有些情境你拿不到圖片的真實尺寸。例如圖片是動態 URL,或是 CMS 沒回傳尺寸欄位,或是用 <div>background-image 做的封面。這時候就輪到 CSS aspect-ratio 上場:

.cover {
  aspect-ratio: 16 / 9;
  width: 100%;
  background-size: cover;
}

aspect-ratio 在需要限制圖片比例時相當方便,像是做正方形卡片、影片 thumbnail、或是把 <iframe> 包成 16:9 的時候。以前要做這件事得用 padding-top: 56.25% 之類的 hack,現在一行就解決了。

回到圖片這邊,原則上:

  • 如果你知道圖片的真實尺寸 → 寫在 HTML 的 width height
  • 如果你不知道,但能決定它顯示的比例 → 用 CSS aspect-ratio
  • 兩個都不衝突,搭配使用也行

換個格式:WebP 跟 AVIF

JPEG 是 1992 年的規格,比我還老。現代格式可以小非常多:

  • WebP — Google 推的格式,2010 年發布。同樣品質下檔案大小可以比 JPEG 小2,所有主流瀏覽器都支援3
  • AVIF — 基於 AV1 影片編碼的圖片格式,2019 年的規格。攝影類照片在中高品質下比 JPEG 小更多4。Safari 從 16 開始支援,Chrome 跟 Firefox 更早,目前主流瀏覽器都已涵蓋5

可以順便看看JPEG 壓縮背後的秘密這篇文章理解一下圖片壓縮的原理。

我自己現在的做法是:能用 AVIF 就用 AVIF,WebP 當備援,JPEG/PNG 當最終 fallback。實際壓出來的差距會因為圖片內容而異——色塊多、漸層少的圖(例如 UI 截圖)WebP/AVIF 壓縮效果非常好;高頻細節多的(例如風景照)差距會小一些,但通常還是值得。

要做格式 fallback,就會用到 <picture>

picture、source、srcset 的搭配

<picture> 的概念其實很單純:列出幾個 <source>,瀏覽器從上往下挑第一個支援的,沒有支援的就 fallback 到 <img>

<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" width="1600" height="900" alt="hero" />
</picture>

到這邊只解決了格式的問題,還沒解決大小的問題。手機螢幕只有 400px 寬,下載一張 1600px 寬的圖片是浪費頻寬。這就要靠 srcsetsizes

用 srcset 提供多種解析度

srcset 有兩種寫法,差別在描述符不一樣:

  • w 描述符:標示圖片的實際寬度(像素)。搭配 sizes 一起用,瀏覽器會根據版面寬度跟 DPR 自己挑
  • x 描述符:標示這張圖是給幾倍 DPR 用的。適合同一個版面尺寸,只是要應付 Retina的場景

x 描述符長這樣,比較單純:

<img
  src="avatar.jpg"
  srcset="avatar.jpg 1x, avatar@2x.jpg 2x, avatar@3x.jpg 3x"
  width="80"
  height="80"
  alt="avatar"
/>

但如果你的圖片在不同螢幕上顯示寬度根本不同(例如手機是滿版、桌機是固定 800px),x 描述符就不夠用了,要用 w 描述符配 sizes

<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg   400w,
    hero-800.jpg   800w,
    hero-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw, 800px"
  width="1600"
  height="900"
  alt="hero"
/>

sizes 是在告訴瀏覽器「這張圖片在版面上實際會顯示多寬」。例如上面寫的是:

  • 螢幕寬度 ≤ 600px 時 → 圖片佔 100vw(整個螢幕寬)
  • 否則 → 圖片固定 800px

瀏覽器拿到這個資訊,再考慮裝置的 DPR(device pixel ratio,物理像素跟 CSS 像素的比值),就會去 srcset 裡挑一個最適合的版本下載。例如 iPhone 螢幕 390px 寬、DPR 是 3,那它需要 390 × 3 = 1170px 的解析度,會挑 1600w 那個;如果是桌機 800px、DPR 是 2,需要 1600px,也會挑 1600w;如果是桌機 800px、DPR 是 1,挑 800w 就夠了。

把格式跟尺寸合在一起

<picture>srcset 組合起來,會長這樣:

<picture>
  <source
    type="image/avif"
    srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
    sizes="(max-width: 600px) 100vw, 800px"
  />
  <source
    type="image/webp"
    srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1600.webp 1600w"
    sizes="(max-width: 600px) 100vw, 800px"
  />
  <img
    src="hero-800.jpg"
    srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
    sizes="(max-width: 600px) 100vw, 800px"
    width="1600"
    height="900"
    alt="hero"
  />
</picture>

每個 <source> 是同一張圖在不同格式下的所有尺寸。所以實際上你可能要產出 3 個格式 × 3 個尺寸 = 9 個檔案。這部分通常會交給 build tool 或 CDN 處理,常見的選擇有 Astro 的 <Image>、Next.js 的 next/image 或圖片 CDN 服務。

我現在很常用 Cloudflare Images。他不需要事先把同一張圖打包成 AVIF/WebP/JPEG 多個版本上傳,而是根據 request 的 Accept header 判斷瀏覽器支援的格式,on-demand 轉檔再回傳,同時保有 CDN 快取。比起在 build 階段就生出各種圖片,動態轉換圖片格式不需要再額外多一道步驟。

為手機換一張不同的圖片(art direction)

前面講的都是同一張圖、不同解析度。但有些時候你會希望手機版跟桌機版根本是「不一樣的圖」,這叫做 art direction

例如桌機版是一張橫幅 16:9 的風景照,但放到手機上整張圖縮小之後主體變得超小、看不清楚。比較好的做法是手機上換成一張裁切過的直幅圖,主體放大。

<picture> 的另一個用途就是這個。在 <source> 上加 media 屬性:

<picture>
  <!-- 手機版:直幅裁切 -->
  <source
    media="(max-width: 600px)"
    type="image/avif"
    srcset="hero-mobile-400.avif 400w, hero-mobile-800.avif 800w"
    sizes="100vw"
  />
  <source
    media="(max-width: 600px)"
    type="image/webp"
    srcset="hero-mobile-400.webp 400w, hero-mobile-800.webp 800w"
    sizes="100vw"
  />

  <!-- 桌機版:橫幅原圖 -->
  <source
    type="image/avif"
    srcset="hero-800.avif 800w, hero-1600.avif 1600w"
    sizes="800px"
  />
  <source
    type="image/webp"
    srcset="hero-800.webp 800w, hero-1600.webp 1600w"
    sizes="800px"
  />

  <img
    src="hero-800.jpg"
    width="1600"
    height="900"
    alt="hero"
  />
</picture>

<source> 一樣是從上往下選第一個符合的,所以media 的要放上面。手機版優先吃前面的 mobile 圖,桌機才會匹配到後面的桌機版本。

有個小細節要注意:手機版的圖片可能是直幅,aspect ratio 跟 <img> 上寫的 16:9 不同。這時候搭配 CSS aspect-ratio 統一管理容器的比例,或在不同斷點下用 CSS 切換,會比依賴 <img> 上的固定屬性穩定。

用 object-fit 收尾

當你預留的空間比例跟圖片本身比例不一致的時候(例如卡片強制做成 1:1 但圖片是 16:9),就會用到 object-fit

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

object-fit<img><video> 都有效。常用的兩個值:

  • cover — 填滿容器,圖片會被裁切。卡片縮圖、大頭照通常用這個
  • contain — 完整顯示圖片,容器多出來的部分留空。logo、商品圖片之類「不能裁」的內容用這個

搭配 object-position 可以調整裁切的對齊位置(例如 object-position: top 讓裁切從頂部開始,避免人物被切到下巴)。

loading 跟 fetchpriority

除了尺寸跟格式之外,還有兩個屬性現在已經是標配:

<img
  src="hero.jpg"
  width="1600"
  height="900"
  loading="lazy"
  decoding="async"
  alt="hero"
/>
  • loading="lazy" — 圖片進入 viewport 才下載。對長文章、商品列表、瀑布流特別有效,能省掉大量初始載入流量
  • decoding="async" — 圖片 decode 不阻塞主執行緒。預設行為其實就接近 async,但寫上去可以避免某些瀏覽器的同步解碼路徑
  • fetchpriority="high" — 反過來,關鍵圖片可以加這個讓瀏覽器優先下載

瀏覽器這幾年已經加入很多圖片優化的機制,不像以前要寫複雜的 JavaScript 自己優化,比以前手動處理輕鬆很多。前端要做的就是把該加的屬性加上去,剩下的交給瀏覽器處理。

Footnotes

  1. Setting Height And Width On Images Is Important Again — Smashing Magazine

  2. WebP Compression Study — Google

  3. Can I Use — WebP

  4. AVIF for Next-Generation Image Coding — Netflix Tech Blog

  5. Can I Use — AVIF

相關文章

探索其他主題