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

HTML, 新手 Jamp Holes, 網站相關

HTML picture srcset 與 sizes 效能優化實作:w 描述符計算與 DPR 載入策略

HTML picture srcset 與 sizes 效能優化實作:w 描述符計算與 DPR 載入策略 特色圖片

Table of Contents

在上一篇 HTML picture 與 source 標籤實作要點:Art Direction 裁切、AVIF/WebP 降級與深色模式適配 與 HTML 行內與區塊元素解析:深入置換元素(img/input)的盒模型與排版邊界 中,我們介紹瞭如何透過 <picture> 與 <source> 解決跨裝置不同長寬比裁切(Art Direction)與 AVIF / WebP 格式降級。而在網站中佔比高達 80% 以上的文章插圖、商品卡片與圖庫列表,其長寬比在桌機與手機上完全相同,真正的效能課題是「如何避免 390px 寬的手機下載一張 2400px 的桌機大圖」。本文將依據 WHATWG HTML Living Standard 規範,解析 HTML picture 與 srcset 在解析度切換(Resolution Switching)中的計算公式、瀏覽器預載掃描器(Preload Scanner)為何需要 sizes 屬性、新標準 sizes="auto" 的應用,並整理國外開發者論壇最常討論的 6 個 srcset 排查問答。


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

如果你目前正在處理手機端依然載入大尺寸圖片、縮放視窗時 srcset 沒有切換,或對 sizes 為什麼不能寫 % 百分比感到困惑,可以直接點擊下方連結跳轉至對應解答與程式碼範本:

如果不趕時間,建議順著下方章節往下讀,我們將從瀏覽器預載掃描器(Preload Scanner)的運作時機開始,逐一講解 srcset 的 w 與 x 描述符差異、sizes 目標像素計算公式與新標準 sizes="auto" 的底層原理。


一、為什麼瀏覽器需要 srcset 與 sizes 來提前計算圖片尺寸?

1. 原本常見的單一高解析度大圖寫法與頻寬浪費

為了讓圖片在 4K 桌機螢幕與高像素密度(Retina)裝置上看起來清晰不模糊,許多專案會直接匯出一張 1920px 或 2400px 寬的高解析度圖片放進 <img>:

<!-- 原本常見的單一大圖寫法:所有裝置被迫下載同一張 1.2 MB 的 2400px 原圖 -->
<img
  src="/images/article-cover-2400.jpg"
  alt="雲端原生微服務監控儀表板"
  width="2400"
  height="1350"
  style="width: 100%; height: auto;"
>

這種寫法雖然在 CSS 的 width: 100% 控制下能自動縮放外觀,但在網路傳輸與記憶體層面卻造成兩個問題:

  1. 行動端頻寬與時間浪費:在三欄卡片列表中,一張顯示寬度僅 350px 的卡片縮圖,若下載 2400px 的原圖,多傳輸了將近 80%~90% 不會被肉眼看見的多餘像素。
  2. 行動裝置影像解碼(Image Decoding)記憶體壓力:瀏覽器下載 2400x1350 的圖片後,在解碼展開為未壓縮點陣圖(RGBA 每像素 4 Bytes)時,單張圖就會在手機記憶體中佔用 2400 × 1350 × 4 ≈ 12.96 MB 的緩衝空間。

2. 瀏覽器預載掃描器(Preload Scanner)與選圖公式:目標像素 = sizes 渲染寬度 × DPR

許多前端工程師在初次接觸 srcset 與 sizes 時,常會有一個疑問:「既然我已經在外部 CSS 檔案裡寫了 .card-img { width: 33.3vw; },為什麼瀏覽器不能自己看 CSS 就好,還要我在 HTML 的 sizes 裡再寫一次寬度?」

原因在於瀏覽器的預載掃描器(Preload Scanner)運作時機:
當瀏覽器剛收到 HTML 文件時,外部的 .css 樣式表與 .js 腳本通常還在網路上下載、尚未完成解析(CSSOM 尚未建立)。為了不讓圖片傻傻等待 CSS 下載完才開始請求,瀏覽器的 Preload Scanner 會在 HTML 解析初期就提前掃描 <img> 標籤並發起圖片下載。

在沒有 CSSOM 的情況下,瀏覽器只知道兩件事:

  1. 當前視窗寬度(Viewport Width,即 100vw)。
  2. 當前螢幕的裝置像素比(Device Pixel Ratio, DPR)(例如一般桌機螢幕為 1x,MacBook Retina 為 2x,現代 iPhone 與高階 Android 手機為 3x)。

因此,我們必須透過 sizes 屬性直接告訴 Preload Scanner:「在某個視窗寬度下,這張圖片預計會佔據多寬的版面槽位(Slot Width)」,瀏覽器再依據以下公式算出目標物理像素:

瀏覽器選圖計算公式:目標物理像素 (Target Physical Pixels) = sizes 算出的槽位寬度 (CSS px) × 裝置像素比 (DPR)

接著瀏覽器便從 srcset 清單中,挑選最接近(通常優先挑選大於等於目標值)的那張 w 圖片下載。下表規格對照表整理三種解析度描述符的規格與適用場景:

規格對照表:x 描述符、w 描述符與 sizes=”auto” 比較

描述符與屬性組合語法範例預設值與規範限制最佳適用場景
x 像素密度描述符srcset="logo-1x.webp 1x, logo-2x.webp 2x, logo-3x.webp 3x"不得與 sizes 或 w 描述符混用;未標註者預設為 1x固定 CSS 寬度的小型元件(如導覽列 160px 品牌 Logo、48px 使用者頭貼)
w 寬度描述符 + 明確 sizessrcset="img-400.webp 400w, img-800.webp 800w" sizes="(max-width: 768px) 100vw, 50vw"若寫了 w 卻漏寫 sizes,瀏覽器預設以 sizes="100vw" 計算;sizes 內嚴禁使用 %首屏關鍵圖片(LCP) 或隨視窗流體縮放的橫幅、多欄卡片圖片
w 寬度描述符 + sizes="auto"loading="lazy" srcset="img-400.webp 400w, img-800.webp 800w" sizes="auto, (max-width: 768px) 100vw, 50vw"必須搭配 loading="lazy"(WHATWG 標準,Chrome 126+ / Safari 18.4+ 支援)首屏以下(Below-the-fold)的懶載入圖片,由瀏覽器在排版完成後自動帶入真實渲染寬度

下圖展示瀏覽器 Preload Scanner 如何讀取 sizes 媒體條件、乘上裝置 DPR,並從 srcset 的 w 候選清單中選出最佳圖片:

瀏覽器 Preload Scanner 結合 sizes、DPR 與 srcset (w 描述符) 之目標圖片計算流程圖

圖:瀏覽器 Preload Scanner 結合 sizes、DPR 與 srcset (w 描述符) 之目標圖片計算流程圖


二、三大常見排版場景的 srcset 與 sizes 程式碼範本

下圖提供選擇 x 像素密度描述符、w 寬度描述符與 sizes="auto" 的實務決策樹:

響應式圖片 x 描述符、w 描述符與 sizes=auto 選型決策樹

圖:響應式圖片 x 描述符、w 描述符與 sizes=auto 選型決策樹

三大實務場景程式碼實作

1. 場景一:固定寬度元件(品牌 Logo 與頭貼)— 使用 x 像素密度描述符

當圖片在所有螢幕尺寸下的 CSS 顯示寬度都是固定的(例如導覽列上的品牌 Logo 永遠固定為 150px × 40px),不需要使用 w 與 sizes,直接用 1x、2x、3x 像素密度描述符最簡潔直觀:

<!-- 固定 150x40 CSS 像素的品牌 Logo:依螢幕 DPR 自動挑選 150w / 300w / 450w 圖檔 -->
<img
  src="/images/brand-logo-150w.webp"
  srcset="/images/brand-logo-150w.webp 1x,
          /images/brand-logo-300w.webp 2x,
          /images/brand-logo-450w.webp 3x"
  alt="企業品牌標誌"
  width="150"
  height="40"
>

2. 場景二:RWD 多欄卡片網格(手機單欄、平板雙欄、桌機三欄)— 使用 w 描述符搭配 sizes

假設我們有一個常見的響應式文章卡片列表:

  • 手機版(<= 640px):單欄滿版顯示,扣除左右各 16px 邊距後寬度為 calc(100vw - 32px)。
  • 平板版(<= 1024px):雙欄並排顯示,每張卡片約佔 calc(50vw - 24px)。
  • 桌機版(> 1024px):最大容器寬度限制為 1200px 的三欄並排,每張卡片固定約 380px。

此時透過 srcset 的 w 描述符(標註每張圖檔真實的像素寬度)搭配 sizes(由上而下匹配第一個符合的媒體條件),即可讓各裝置精準下載最合適的尺寸:

<img
  src="/images/card-cover-800.webp"
  srcset="/images/card-cover-400.webp 400w,
          /images/card-cover-800.webp 800w,
          /images/card-cover-1200.webp 1200w"
  sizes="(max-width: 640px) calc(100vw - 32px),
         (max-width: 1024px) calc(50vw - 24px),
         380px"
  alt="分散式資料庫讀寫分離架構圖"
  width="800"
  height="450"
  loading="lazy"
  decoding="async"
  style="width: 100%; height: auto; display: block;"
>

3. 場景三:結合 picture 格式降級與 srcset 多解析度矩陣的複合寫法

當我們想要**同時享有 <picture> 的「AVIF ➔ WebP ➔ JPEG 新格式降級」與 srcset 的「多解析度自動挑選」**時,可以在 <picture> 的每個 <source> 上同時配置 type、srcset(含 w 描述符)與 sizes:

<picture>
  <!-- 第一順位:支援 AVIF 的瀏覽器,依 sizes 與 DPR 從三種寬度中挑選一張 AVIF -->
  <source
    type="image/avif"
    srcset="/images/hero-640.avif 640w,
            /images/hero-1280.avif 1280w,
            /images/hero-1920.avif 1920w"
    sizes="(max-width: 1280px) 100vw, 1280px"
  >

  <!-- 第二順位:支援 WebP 的瀏覽器,從三種寬度中挑選一張 WebP -->
  <source
    type="image/webp"
    srcset="/images/hero-640.webp 640w,
            /images/hero-1280.webp 1280w,
            /images/hero-1920.webp 1920w"
    sizes="(max-width: 1280px) 100vw, 1280px"
  >

  <!-- 最終保底:傳統 JPEG 與基礎屬性宣告 -->
  <img
    src="/images/hero-1280.jpg"
    srcset="/images/hero-640.jpg 640w,
            /images/hero-1280.jpg 1280w,
            /images/hero-1920.jpg 1920w"
    sizes="(max-width: 1280px) 100vw, 1280px"
    alt="年度開發者大會主題橫幅"
    width="1920"
    height="1080"
    fetchpriority="high"
    style="width: 100%; height: auto; display: block;"
  >
</picture>

三、WHATWG 新標準:loading=”lazy” 搭配 sizes=”auto” 省去手算斷點

在大型元件庫中,一個通用的 <ImageCard> 元件可能會被放進雙欄、三欄、四欄或側邊欄容器裡,元件本身很難預先知道自己在不同頁面下的 sizes 媒體查詢公式。

為了解決這個痛點,WHATWG HTML Living Standard 正式納入 sizes="auto" 規範(已在 Chrome 126+、Edge 126+、Safari 18.4+ 支援):
因為設定了 loading="lazy" 的懶載入圖片,只會在捲動接近可視範圍時才發起請求,而此時 CSS 樣式表早已解析完畢、瀏覽器已經知道該 <img> 在畫面上的真實渲染像素寬度(Concrete Object Size)!

只要將 sizes 開頭寫上 auto(並在逗號後面保留傳統 sizes 數值作為舊版瀏覽器退路),支援的瀏覽器就會自動拿 CSS 算出的真實寬度乘上 DPR 去選圖:

<!-- 現代懶載入圖片推薦寫法:sizes 以 auto 開頭,後方保留舊版瀏覽器 fallback -->
<img
  loading="lazy"
  src="/images/gallery-800.webp"
  srcset="/images/gallery-400.webp 400w,
          /images/gallery-800.webp 800w,
          /images/gallery-1200.webp 1200w"
  sizes="auto, (max-width: 768px) 100vw, 33vw"
  width="800"
  height="600"
  alt="作品集展示卡片"
  style="width: 100%; height: auto;"
>

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

以下彙整 Stack Overflow、Reddit(r/webdev、r/Frontend)與 WebPageTest 效能論壇中,開發者針對 srcset 與 sizes 最常提問的 6 個問題:

Q1:為什麼寫了 srcset 的 w 描述符卻沒寫 sizes 時,三欄卡片縮圖在手機與桌機上都抓 100vw 最大圖?

  • 常見情境:開發者在桌機四欄商品列表的 <img> 寫了 srcset="item-300.jpg 300w, item-600.jpg 600w, item-1200.jpg 1200w",但省略了 sizes 屬性。結果在 1440px 的桌機上,每張實際只顯示 300px 寬的小卡片,全部去下載了 1200w 的大圖!
  • 底層原因:根據 WHATWG HTML 規範,當 srcset 使用 w 寬度描述符且未明確指定 sizes 時,sizes 的預設值為 100vw(即假設圖片佔滿整個視窗寬度)。因此在 1440px 視窗下,瀏覽器會以 1440px 為基準去挑選 1200w。
  • 解法:凡是在 srcset 中使用了 w 描述符,就必須同時搭配 sizes 屬性(如 sizes="(max-width: 768px) 50vw, 300px",或對懶載入圖片加上 sizes="auto, ...")。

Q2:為什麼在桌機將瀏覽器視窗從寬拉窄時,Chrome 沒有自動切換去下載小尺寸的圖片?

  • 常見情境:開發者寫好 srcset 與 sizes 後,在全螢幕桌機打開網頁(此時瀏覽器載入了 1600w 大圖),接著用滑鼠把瀏覽器視窗拉窄到 400px 手機寬度,打開 Network 面板卻發現瀏覽器完全沒有去請求 400w 的小圖,懷疑 srcset 寫錯了。
  • 底層原因:這是 Chromium 與其他現代瀏覽器刻意設計的頻寬保護機制!因為 srcset 內的所有圖片被視為「同一構圖的不同解析度副本」,既然瀏覽器記憶體快取(Memory Cache)中已經擁有一張高畫質的 1600w 圖片,直接將現有的 1600w 縮小顯示在 400px 容器內完全不花一毛網路流量;若為了視窗變窄又發起 HTTP 請求去下載一張畫質較低的 400w 圖片,反而浪費頻寬。(反之,若從窄視窗拉寬到大視窗,瀏覽器才會補抓大圖。)
  • 解法:在 Chrome DevTools 測試 srcset 時,請遵循以下三個步驟:
    1. 打開 DevTools 切換至 Network 分頁,勾選 Disable cache(停用快取)。
    2. 點擊左上角開啟 Device Toolbar(裝置模擬器,Ctrl+Shift+M),先選好目標手機型號(如 iPhone 14 Pro)。
    3. 按下 Ctrl+R 重新整理頁面,即可觀察到瀏覽器從零開始評估並下載對應尺寸的圖檔。

Q3:為什麼我的手機螢幕寬度明明只有 390px,瀏覽器卻跳過 400w 與 800w,直接下載 1200w 的圖片?

  • 常見情境:開發者在 iPhone(視窗寬度 390px)上實測滿版圖片(sizes="100vw"),預期瀏覽器會挑選 400w 的圖片,但從 Network 面板看卻發現它下載了 1200w。
  • 底層原因:混淆了 CSS 邏輯像素(CSS Pixels) 與 裝置物理像素(Physical Pixels)。現代 iPhone 與旗艦 Android 手機的螢幕像素密度(window.devicePixelRatio, DPR)普遍為 3。當 sizes="100vw" 時,瀏覽器計算出的目標物理像素為 390 CSS px × 3 (DPR) = 1170 物理像素,為了在 Retina 螢幕呈現銳利畫質,自然會挑選最接近的 1200w。
  • 解法:這是完全符合規範的正常行為。若圖片屬於視覺容忍度較高的背景或次要配圖、且希望在 3x 手機上強制限制傳輸體積,可調整 sizes 的最大上限(例如將滿版卡片設為最大 800px 上限,或交由高壓縮比的 AVIF 格式降低 1200w 的檔案大小)。

Q4:可以在同一個 srcset 屬性裡面同時混寫 400w(寬度描述符)與 2x(像素密度描述符)嗎?

  • 常見情境:開發者嘗試寫出 srcset="small.jpg 400w, retina.jpg 2x",結果 W3C HTML Validator 直接報錯,且部分候選圖片被瀏覽器完全忽略。
  • 底層原因:根據 WHATWG HTML 規範,同一個 srcset 屬性中的所有候選項目必須使用同一種描述符系統:要嘛全部使用 w 寬度描述符,要嘛全部使用 x 像素密度描述符,嚴禁混用;此外,若使用了 x 描述符,則不得在同一個標籤上宣告 sizes 屬性。
  • 解法:流體縮放圖片統一全部寫 w 描述符(如 400w, 800w, 1200w)並搭配 sizes;固定寬度小圖示統一全部寫 x 描述符(如 1x, 2x)並省略 sizes。

Q5:為什麼在 sizes 屬性裡面寫百分比(例如 sizes=”50%”)會完全失效?

  • 常見情境:因為圖片在父容器內設定了 CSS width: 50%,開發者很直覺地寫出 sizes="(max-width: 768px) 100%, 50%",結果瀏覽器完全無視這段設定,直接退回預設的 100vw。
  • 底層原因:根據 WHATWG 規範,sizes 中的 <source-size-value> 長度值明確禁止使用百分比(%)!因為百分比是相對於「父容器寬度」計算的,而在瀏覽器 Preload Scanner 執行當下,DOM 樹與外部 CSS 根本還沒完成排版,瀏覽器無從得知父容器有多寬,唯一已知的只有視窗寬度(vw)。
  • 解法:在 sizes 中請一律使用 vw(視窗寬度單位)、px / rem / em(絕對或字級相對單位),或搭配 calc()(例如雙欄扣除間距寫成 calc(50vw - 24px))。

Q6:實務專案中該準備幾個 srcset 圖片寬度斷點,才不會讓雲端儲存空間與建置時間暴增?

  • 常見情境:有些教學示範每隔 100px 就切一張圖(300w, 400w, 500w ... 2000w 共 18 張圖),若再乘上 AVIF、WebP、JPG 三種格式,上傳一張圖就會在 S3 產生 54 個檔案,且大幅降低 CDN 快取命中率(Cache Hit Ratio)。
  • 底層原因:斷點切得太密,會讓不同螢幕尺寸的使用者各自請求不同 URL,導致 CDN 邊緣節點快取碎片化。
  • 解法:在生產環境中,每張圖片準備 3~5 個具有顯著級距的寬度斷點即能取得頻寬與 CDN 快取命中率的最佳平衡:
    • 通用內容圖片/橫幅(4 段式):400w(1x 手機 / 縮圖)、800w(2x 手機 / 平板)、1200w(3x 手機 / 一般筆電)、1920w(高解析度桌機)。
    • 卡片縮圖(3 段式):360w、720w、1080w。

總結

善用 HTML picture 與 srcset 的解析度切換機制,能在不犧牲高解析度螢幕畫質的前提下,大幅降低行動裝置的圖片傳輸量:

  1. 理解 Preload Scanner 公式:瀏覽器以 sizes 槽位寬度 × DPR 決定目標物理像素,因此使用 w 描述符時務必提供準確的 sizes(且切勿在 sizes 內寫 % 百分比)。
  2. 區分 x 與 w 描述符:固定寬度的 Logo 與頭貼使用 1x, 2x, 3x;隨版面縮放的卡片與橫幅使用 400w, 800w, 1200w 搭配 sizes。
  3. 導入新標準 sizes="auto":針對設定了 loading="lazy" 的非首屏圖片,可加上 sizes="auto, ..." 讓瀏覽器直接使用排版後的真實寬度。
  4. 正確測試快取行為:在 Chrome DevTools 驗證 srcset 時,務必勾選 Disable cache 並先切好手機模擬器再重新整理。

參考資料與延伸閱讀

發表迴響