紀錄工作經驗、相關知識,解決技術相關問題。

HTML, 新手 Jamp Holes, 網站相關

HTML picture 與 source 標籤語法教學:Art Direction 裁切、AVIF 降級與深色模式實作

HTML picture 與 source 標籤語法教學:Art Direction 裁切、AVIF 降級與深色模式實作 特色圖片

Table of Contents

本文將詳細解析 HTML picture 與 source 的核心規範、底層運作機制與實務配置要點,協助開發者在專案中快速排查常見問題並落實標準寫法。

在先前的 HTML 行內與區塊元素解析:深入置換元素(img/input)的盒模型與排版邊界 與 什麼是 DOM Tree?解析瀏覽器渲染機制與 Reflow / Repaint 效能優化實作 中,我們探討了 <img> 作為可置換元素(Replaced Element)的尺寸計算與版面重排機制。當實務專案需要「在桌機顯示 16:9 橫幅全景、在手機顯示 4:5 直式人物特寫(Art Direction)」,或是「優先載入高壓縮比的 AVIF / WebP 並向後相容 JPEG」時,單靠一個 <img> 標籤便難以兼顧構圖與格式降級。本文將依據 WHATWG HTML Living Standard 規範,解析 HTML picture 與 source(<picture> 與 <source>)的由上而下匹配機制、各斷點獨立長寬比設定、零 JS 深色模式切換,並整理國外開發者論壇最常討論的 6 個實務問答。


常見問題(Q&A)與程式碼範本快速跳轉

如果你目前正在處理 <picture> 圓角或寬高樣式失效、不同螢幕裁切比例導致畫面跳動(CLS),或是透過 JavaScript 讀取不到實際載入的圖片網址等問題,可以直接點擊下方連結跳轉至對應解答與程式碼範本:

如果不趕時間,建議順著下方章節往下讀,我們將從單一 img 標籤在跨裝置構圖上的限制開始,逐一講解 picture 與 source 的 First-Match 匹配規則、多比例防位移配置與次世代圖片格式的底層運作原理。


一、為什麼跨裝置構圖裁切(Art Direction)與多格式降級需要 picture 標籤?

1. 原本正常的標準 img 寫法與兩個設計瓶頸

在一般網頁中,若圖片在所有螢幕尺寸都維持相同的長寬比與構圖,標準寫法是使用單一 <img> 標籤:

<!-- 原本正常的單一構圖圖片寫法 -->
<img
  src="/images/product-hero.jpg"
  alt="新款無線降噪耳機產品主視覺"
  width="1200"
  height="600"
  style="max-width: 100%; height: auto;"
>

然而,在電商首頁橫幅(Hero Banner)或行銷著陸頁中,這種單一圖片寫法會遇到兩個明確瓶頸:

  1. 構圖焦點在手機螢幕上過小(Art Direction 問題):
    一張 1600x600 的桌機橫幅圖片,通常左側是宣傳文案或留白背景、右側才是產品主體。當直接等比例縮小到 375px 寬的手機螢幕時,高度只剩下 140px,畫面中的產品主體變得極為細小;此時設計師會希望手機版改載入一張 800x1000(4:5 直式構圖)的產品特寫圖。
  2. 次世代圖片格式(AVIF / WebP)的漸進式降級需求:
    AVIF(image/avif)與 WebP(image/webp)相比傳統 JPEG 可節省 30%~50% 以上的檔案體積,但為了確保舊版環境或特殊內嵌瀏覽器也能正常顯示圖片,我們需要一套宣告式的「優先嘗試 AVIF ➔ 次選 WebP 延長 ➔ 最終保底 JPEG」降級機制。

2. WHATWG 規範下 picture、source 與 img 的協作關係與速查表

初學者最容易誤解的一點,是以為 <picture> 是一個用來「取代 <img>」的新圖片標籤。事實上,根據 WHATWG HTML Living Standard 規範:

  • <picture> 本身是一個零視覺外觀的透明包裝容器(Wrapper):它本身不渲染任何像素,唯一的工作是負責管理內部的 <source> 條件清單,並將挑選出來的最佳圖片網址交給底部的 <img> 子元素。
  • <source> 是純粹的候選規則宣告(空元素 Void Element):負責提供 media(CSS 媒體查詢條件)、type(IANA MIME Type 格式條件)與 srcset(候選圖片網址),以及對應的 width 與 height 尺寸。
  • <img> 才是真正被渲染在畫面上的可置換元素:所有的無障礙替代文字(alt)、圓角與陰影(border-radius / box-shadow)、object-fit 以及載入策略(loading / fetchpriority),都必須寫在最後這顆 <img> 標籤上。

下表整理三種圖片載入方案的職責邊界(關於純解析度切換 srcset 的 w 寬度描述符與 sizes 計算公式,我們將在下一篇專文詳細探討):

方案類型標籤結構核心解決問題是否支援跨斷點不同長寬比(Art Direction)是否支援多 MIME 格式降級(AVIF/WebP/JPG)
基礎圖片<img src="...">固定尺寸與單一格式圖片載入否否
同構圖多解析度<img srcset="..." sizes="...">相同構圖與長寬比下,依螢幕寬度與 DPR 下載不同像素尺寸否(瀏覽器可依快取自由決定抓哪張)否
宣告式條件切換<picture> + <source> + <img>強制依 Media Query 切換不同裁切構圖、依 type 漸進降級新格式、深色模式圖片切換是(100% 嚴格遵循 media 條件)是(依 type 順序匹配)

下圖展示瀏覽器解析 <picture> 時,如何由上而下依序執行「第一個命中即採用(First-Match Wins)」規則,並將結果注入底層 <img> 元素:

HTML picture 與 source 由上而下 First-Match 匹配機制與 img 渲染關係圖

圖:HTML picture 與 source 由上而下 First-Match 匹配機制與 img 渲染關係圖


二、HTML picture 與 source 三大生產環境程式碼範本

下圖彙整實務上最常運用 <picture> 與 <source> 的三大宣告式場景(Art Direction 構圖裁切、AVIF/WebP 格式降級、系統深色與減速偏好適配)及其與底層 <img> 的互動關係:

picture 標籤三大條件切換場景(Art Direction、AVIF/WebP 降級、深色模式)與 LCP 預載架構圖

圖:picture 標籤三大條件切換場景(Art Direction、AVIF/WebP 降級、深色模式)與 LCP 預載架構圖

1. 場景一:Art Direction 跨斷點構圖裁切(為每個 source 配置獨立 width 與 height)

當桌機版圖片為 1600x600(8:3 橫幅),而手機版圖片為 800x1000(4:5 直圖)時,若只在底部 <img> 寫上 width="800" height="1000",桌機瀏覽器在圖片下載完成前會錯誤地用 4:5 的比例預留高度,圖片載入後瞬間縮回 8:3,引發嚴重的累積版面配置轉移(CLS)。

根據現行 WHATWG HTML 規範,<source> 標籤原生支援獨立的 width 與 height 屬性!只要在每個 <source> 標註該裁切版本對應的真實像素寬高,並搭配 CSS width: 100%; height: auto;,瀏覽器就能在各個斷點精準預留對應的長寬比空間:

<picture>
  <!-- 1. 桌機版(>= 1024px):載入 1600x600 (8:3) 橫向全景圖,並宣告對應寬高 -->
  <source
    media="(min-width: 1024px)"
    srcset="/images/campaign-desktop-1600x600.webp"
    type="image/webp"
    width="1600"
    height="600"
  >

  <!-- 2. 平板版(>= 768px):載入 1024x640 (16:10) 中型構圖,並宣告對應寬高 -->
  <source
    media="(min-width: 768px)"
    srcset="/images/campaign-tablet-1024x640.webp"
    type="image/webp"
    width="1024"
    height="640"
  >

  <!-- 3. 手機版與預設保底(< 768px):載入 800x1000 (4:5) 直式特寫圖 -->
  <img
    src="/images/campaign-mobile-800x1000.jpg"
    alt="秋季戶外露營裝備特輯主視覺"
    width="800"
    height="1000"
    style="width: 100%; height: auto; display: block; border-radius: 12px;"
  >
</picture>

2. 場景二:次世代圖片格式漸進降級(AVIF ➔ WebP ➔ JPEG)

由於瀏覽器評估 <source> 是嚴格按照 DOM 順序「由上往下找第一個支援的項目」,因此必須將壓縮率最高、格式最新的 image/avif 放在最上方,接著放 image/webp,最後以 <img> 的 .jpg 或 .png 作為相容保底:

<picture>
  <!-- 第一順位:若瀏覽器支援 AVIF,立即採用並忽略下方所有標籤 -->
  <source srcset="/images/architecture-diagram.avif" type="image/avif">

  <!-- 第二順位:若不支援 AVIF 但支援 WebP,採用此項目 -->
  <source srcset="/images/architecture-diagram.webp" type="image/webp">

  <!-- 最終保底:所有瀏覽器皆支援的標準 PNG / JPEG -->
  <img
    src="/images/architecture-diagram.png"
    alt="微服務閘道器與快取叢集架構圖"
    width="1200"
    height="675"
    loading="lazy"
    decoding="async"
    style="max-width: 100%; height: auto;"
  >
</picture>

3. 場景三:零 JavaScript 自動適配「深色模式」與「減少動態效果(無障礙)」

<source> 的 media 屬性不僅能寫視窗寬度(min-width),還能直接使用任何標準的 CSS 使用者偏好媒體特性(User Preference Media Features):

  • (prefers-color-scheme: dark):在技術文件或 SaaS 產品官網中,當使用者的作業系統切換為深色模式時,自動將「亮色背景的產品截圖/架構圖」替換為「深色背景版本」,避免在深色頁面上出現刺眼的白底圖片。
  • (prefers-reduced-motion: reduce):當使用者在系統無障礙設定中開啟「減少動態效果」時,自動將動態展示圖(如動態 WebP 或 GIF)替換為靜態封面圖,符合 WCAG 2.2 SC 2.2.2(Pause, Stop, Hide)無障礙規範。
<!-- 範例 A:跟隨系統深色/淺色模式自動切換截圖(零 JS) -->
<picture>
  <source
    media="(prefers-color-scheme: dark)"
    srcset="/images/dashboard-preview-dark.webp"
    type="image/webp"
  >
  <img
    src="/images/dashboard-preview-light.webp"
    alt="專案數據分析儀表板介面預覽"
    width="1280"
    height="720"
    style="max-width: 100%; height: auto;"
  >
</picture>

<!-- 範例 B:尊重使用者 prefers-reduced-motion 無障礙設定,停用自動播放動圖 -->
<picture>
  <source
    media="(prefers-reduced-motion: reduce)"
    srcset="/images/feature-demo-static.webp"
    type="image/webp"
  >
  <img
    src="/images/feature-demo-animated.webp"
    alt="拖放排程操作流程示範"
    width="960"
    height="540"
    loading="lazy"
  >
</picture>

三、首屏 LCP 主視覺優化與 JavaScript currentSrc 狀態檢測

1. 首屏 LCP 圖片嚴禁加 loading="lazy",並正確配置互斥 preload

若頁面的最大內容繪製(Largest Contentful Paint, LCP)元素是一張包裹在 <picture> 內的主視覺圖片,請遵守以下兩項效能原則:

  1. 移除 loading="lazy" 並加上 fetchpriority="high":首屏可視範圍內的圖片若加上 loading="lazy",會延後到瀏覽器完成版面配置(Layout)後才開始發起網路請求;應在內部 <img> 明確標註 fetchpriority="high"。
  2. 若在 <head> 使用 <link rel="preload"> 預載 Art Direction 圖片,務必設定互斥的 media 範圍:
    如果你在 <head> 寫了兩個 <link rel="preload" as="image">,一個寫 media="(min-width: 768px)",另一個沒寫 media,桌機瀏覽器會把兩張圖片全部下載下來,造成雙倍頻寬浪費!解決思路是使用完全互斥的媒體查詢條件(如 (min-width: 768px) 與 (max-width: 767.98px)):
<head>
  <!-- 桌機與平板預載橫幅圖(與下方手機版預載條件 100% 互斥,確保只下載一張) -->
  <link
    rel="preload"
    as="image"
    href="/images/hero-desktop.avif"
    type="image/avif"
    media="(min-width: 768px)"
    fetchpriority="high"
  >
  <!-- 手機版預載直式圖 -->
  <link
    rel="preload"
    as="image"
    href="/images/hero-mobile.avif"
    type="image/avif"
    media="(max-width: 767.98px)"
    fetchpriority="high"
  >
</head>

2. 在 JavaScript 中透過 img.currentSrc 讀取真實載入的圖片網址

當瀏覽器透過 <picture> 選中了某一個 <source> 時,它並不會修改 <img src="..."> 的 HTML 屬性值。因此若你在 JavaScript 中讀取 imgEl.src,永遠只會拿到寫在 <img> 上的保底網址。欲取得瀏覽器實際採用的圖片網址,必須讀取唯讀屬性 imgEl.currentSrc:

const heroImg = document.querySelector('picture > img');

// 待圖片載入完成(或視窗尺寸跨越 media 斷點重新選圖)時觸發
heroImg.addEventListener('load', () => {
  console.log('HTML 保底屬性 (img.src):', heroImg.src);
  console.log('瀏覽器實際渲染網址 (img.currentSrc):', heroImg.currentSrc);
});

四、國外開發者論壇(Stack Overflow / Reddit)最常遇到的 6 個 picture 與 source 疑問

以下整理 Stack Overflow、Reddit(r/webdev、r/css)與 Web.dev 社群中,開發者針對 <picture> 與 <source> 最常詢問的 6 個實務問題:

1. 疑問一:為什麼把 class、border-radius 或 alt 寫在 picture 或 source 標籤上完全沒有效果?

  • 常見情境:開發者寫了 <picture class="rounded-xl shadow-md" alt="主視覺"> 或是把 object-fit: cover 寫在 <picture> 與 <source> 上,卻發現圖片完全沒有圓角、object-fit 失效,且 Lighthouse 無障礙檢測報錯「圖片缺少 alt 屬性」。
  • 底層原因:根據 WHATWG 規範,<source> 是不具渲染盒模型的空元素(預設 display: none),而 <picture> 僅是一個預設 display: inline 的透明邏輯容器。真正繪製圖片像素並暴露給無障礙樹(role="img")的實體,自始至終都是最底部的那顆 <img> 標籤。
  • 解法:
    alt、width、height、loading、decoding 與 fetchpriority 屬性一律寫在 <img> 上。
    控制圖片尺寸、圓角(border-radius)與裁切(object-fit)的 CSS 樣式一律直接套用在 <img>(如 picture > img)上;若需要在外層做 Flex/Grid 排版定位,則可將 <picture> 設為 display: block。

2. 疑問二:桌機版橫圖(16:9)與手機版直圖(4:5)比例不同,只寫 img 的 width 與 height 為什麼會變形或觸發 CLS 位移?

  • 常見情境:開發者在 <picture> 內設定了桌機橫圖與手機直圖,為了防範 CLS 在底部 <img> 寫了 width="800" height="1000"。結果在桌機上,若 CSS 沒寫 height: auto,橫圖會被硬拉成直圖變形;若寫了 height: auto,桌機初次渲染時會先撐開 4:5 的超高空白區,等圖片載入後才縮回 16:9,造成版面劇烈跳動。
  • 底層原因:現代瀏覽器會利用 HTML 上的 width 與 height 屬性自動計算內部預設樣式 aspect-ratio: attr(width) / attr(height)。若 <source> 沒有宣告自己的 width 與 height,瀏覽器在桌機端也會套用底部 <img> 的 800 / 1000 比例來預留空間。
  • 解法:如本文第二章第一節所示,為每一個長寬比不同的 <source> 都明確寫上對應的 width 與 height 屬性,並在 <img> 的 CSS 設定 width: 100%; height: auto;。瀏覽器在命中某個 <source> 時,就會自動改用該 <source> 的長寬比預留版面空間。

3. 疑問三:瀏覽器明明載入了 AVIF 圖片,為什麼用 JavaScript 讀取 img.src 卻還是拿到保底的 .jpg 網址?

  • 常見情境:開發者在撰寫圖片燈箱(Lightbox)放大檢視或效能監控腳本時,讀取 event.target.src,發現回傳的總是 <img src="fallback.jpg">,而不是 <source> 中實際顯示的 banner.avif。
  • 底層原因:img.src 反映的是 DOM 元素上靜態的 src 屬性值;而 <picture> 與 srcset 的資源選擇結果是儲存在 HTMLImageElement 的內部選取狀態中。
  • 解法:在 JavaScript 中讀取 img.currentSrc(唯讀字串)。請注意,由於瀏覽器選圖是非同步進行的,若在 DOM 剛插入的同步瞬間讀取,currentSrc 可能為空字串 "",應在 img 的 load 事件中讀取。

4. 疑問四:為什麼把 source type="image/jpeg" 寫在 image/avif 上方,Chrome 就永遠不會載入 AVIF?

  • 常見情境:開發者以為瀏覽器會自動從所有 <source> 裡面「挑選檔案最小或格式最新的一張」,於是隨手把 <source srcset="photo.jpg" type="image/jpeg"> 放在第一行,把 <source srcset="photo.avif" type="image/avif"> 放在第二行,結果發現所有瀏覽器都只下載 .jpg。
  • 底層原因:<picture> 的規範演算法並非「自動挑最優格式」,而是嚴格的「由上而下第一個匹配者勝出(First-Match Wins)」。因為所有瀏覽器都支援第一行的 image/jpeg,檢查到第一行時條件即成立,瀏覽器便直接採用並停止往下看。
  • 解法:排列 <source> 時必須遵循「壓縮率最高/條件最嚴格的放最上面,通用性最高的放最下面」:
    格式順序:image/avif ➔ image/webp ➔ 底部 <img src="...jpg">。
    min-width 斷點順序:media="(min-width: 1280px)" ➔ media="(min-width: 768px)" ➔ 底部 <img>(若把 min-width: 768px 放在 1280px 上方,在 1440px 桌機上也會因為先命中 768px 而永遠抓不到 1280px 的圖!)。

5. 疑問五:為什麼在 picture 裡面只放 source 標籤、省略最後的 img 標籤,畫面上就完全不會顯示圖片?

  • 常見情境:開發者受 <video> 或 <audio> 標籤習慣影響(在 <video> 內只放 <source> 就能播放),於是在 <picture> 內只寫了兩個 <source srcset="..."> 而沒有寫 <img>,結果畫面上完全空白且寬高為 0x0。
  • 底層原因:<video> 與 <audio> 本身就是媒體渲染元素;但 <picture> 只是為子元素 <img> 提供圖片候選清單的輔助容器。根據 WHATWG 規範,<picture> 的內容模型規定必須包含零個或多個 <source>,後面緊接著一個必要的 <img> 元素。若缺少 <img>,<picture> 就沒有可注入圖片的渲染目標。
  • 解法:任何 <picture> 結構的最後一個子元素,永遠必須是一顆完整的 <img> 標籤。

6. 疑問六:如何不寫任何 JavaScript,讓網頁圖片自動跟隨系統「深色模式」或「減少動態效果」切換?

  • 常見情境:過去為了在深色模式切換圖片,常需要用 CSS 同時載入兩張圖並用 display: none 切換,或是用 JavaScript window.matchMedia('(prefers-color-scheme: dark)') 監聽切換 img.src。
  • 底層原因:<source> 的 media 屬性支援完整的標準媒體查詢,且當使用者的系統外觀在深色與淺色之間即時切換時,<picture> 會自動重新評估 media 條件並即時換圖,且在初始載入時只會下載符合當前模式的那一張圖片(不會像雙 <img> + display: none 那樣多浪費一次網路請求)。
  • 解法:在 <picture> 第一行放置 <source media="(prefers-color-scheme: dark)" srcset="dark.webp">,底部 <img> 放置預設亮色圖 src="light.webp" 即可。

總結

掌握 HTML picture 與 source 標籤的底層規則,能讓我們以純宣告式 HTML 解決複雜的跨裝置圖片適配問題:

  1. 認清職責分工:<picture> 是透明容器、<source> 是條件規則,而底部的 <img> 才是真正承載 alt、CSS 樣式與盒模型的渲染核心。
  2. 嚴守由上而下 First-Match 順序:將 image/avif 與較大的 min-width 斷點排在上方,並為不同構圖比例的 <source> 標註各自的 width 與 height 以消除 CLS。
  3. 善用 img.currentSrc 與互斥預載:在 JS 中使用 currentSrc 確認實際載入資源,並為首屏 LCP 圖片配置 fetchpriority="high"。
  4. 區分構圖切換與解析度切換:當你不需要改變圖片裁切比例,而是希望同一構圖在不同螢幕寬度與像素密度(Retina 2x / 3x)下自動挑選最佳檔案大小時,則應搭配 srcset 的 w 描述符與 sizes 屬性——我們將在下一篇 HTML picture 與 srcset 響應式圖片效能:w 寬度描述符、sizes 計算與 DPR 載入策略 中逐一拆解其計算公式與快取排查技巧。

參考資料與延伸閱讀

發表迴響