本文將詳細解析 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 讀取不到實際載入的圖片網址等問題,可以直接點擊下方連結跳轉至對應解答與程式碼範本:
- 國外論壇高頻問答(Q&A):
Q1:為什麼把 class、border-radius 或 alt 寫在 picture 或 source 標籤上完全沒有效果?
Q2:桌機版橫圖(16:9)與手機版直圖(4:5)比例不同,只寫 img 的 width 與 height 為什麼會變形或觸發 CLS 位移?
Q3:瀏覽器明明載入了 AVIF 圖片,為什麼用 JavaScript 讀取 img.src 卻還是拿到保底的 .jpg 網址?
Q4:為什麼把 source type="image/jpeg" 寫在 image/avif 上方,Chrome 就永遠不會載入 AVIF?
Q5:為什麼在 picture 裡面只放 source 標籤、省略最後的 img 標籤,畫面上就完全不會顯示圖片?
Q6:如何不寫任何 JavaScript,讓網頁圖片自動跟隨系統「深色模式」或「減少動態效果」切換? - 生產環境速查表與程式碼範本:
跳轉至:單一 img、img + srcset 與 picture + source 職責邊界對照表
跳轉至:Art Direction 裁切、AVIF/WebP 降級與深色模式三大 HTML 範本
跳轉至:首屏 LCP 主視覺搭配 picture 的 link rel="preload" 預載寫法
如果不趕時間,建議順著下方章節往下讀,我們將從單一 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)或行銷著陸頁中,這種單一圖片寫法會遇到兩個明確瓶頸:
- 構圖焦點在手機螢幕上過小(Art Direction 問題):
一張1600x600的桌機橫幅圖片,通常左側是宣傳文案或留白背景、右側才是產品主體。當直接等比例縮小到375px寬的手機螢幕時,高度只剩下140px,畫面中的產品主體變得極為細小;此時設計師會希望手機版改載入一張800x1000(4:5直式構圖)的產品特寫圖。 - 次世代圖片格式(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 三大生產環境程式碼範本
下圖彙整實務上最常運用 <picture> 與 <source> 的三大宣告式場景(Art Direction 構圖裁切、AVIF/WebP 格式降級、系統深色與減速偏好適配)及其與底層 <img> 的互動關係:
圖: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> 內的主視覺圖片,請遵守以下兩項效能原則:
- 移除
loading="lazy"並加上fetchpriority="high":首屏可視範圍內的圖片若加上loading="lazy",會延後到瀏覽器完成版面配置(Layout)後才開始發起網路請求;應在內部<img>明確標註fetchpriority="high"。 - 若在
<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切換,或是用 JavaScriptwindow.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 解決複雜的跨裝置圖片適配問題:
- 認清職責分工:
<picture>是透明容器、<source>是條件規則,而底部的<img>才是真正承載alt、CSS 樣式與盒模型的渲染核心。 - 嚴守由上而下 First-Match 順序:將
image/avif與較大的min-width斷點排在上方,並為不同構圖比例的<source>標註各自的width與height以消除 CLS。 - 善用
img.currentSrc與互斥預載:在 JS 中使用currentSrc確認實際載入資源,並為首屏 LCP 圖片配置fetchpriority="high"。 - 區分構圖切換與解析度切換:當你不需要改變圖片裁切比例,而是希望同一構圖在不同螢幕寬度與像素密度(Retina 2x / 3x)下自動挑選最佳檔案大小時,則應搭配
srcset的w描述符與sizes屬性——我們將在下一篇 HTML picture 與 srcset 響應式圖片效能:w 寬度描述符、sizes 計算與 DPR 載入策略 中逐一拆解其計算公式與快取排查技巧。
參考資料與延伸閱讀
- MDN Web Docs:picture 元素規格與用法說明
- MDN Web Docs:source 元素與 media、type 屬性
- WHATWG HTML Living Standard:The picture element 規範細節
- MDN Web Docs:Using responsive images in HTML 官方技術規格
- MDN Web Docs:CSS prefers-color-scheme 媒體查詢與深色模式實作
- 延伸閱讀:別再只用 div 與 span:HTML 內聯與區塊元素(Inline vs Block)底層渲染邏輯與常見語法地雷
- 延伸閱讀:React Console Warning:images jsx-a11y/alt-text 警告原因、解決方法與 img alt 設定規範


發表迴響