iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Modern Web

用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)系列 第 19

不同螢幕要不同圖,Astro 響應式圖片怎麼設定?

  • 分享至 

  • xImage
  •  

<Picture> 會先依 <source> 順序與瀏覽器支援度選格式,再由該格式的 srcsetsizes
與裝置像素比(DPR)決定實際下載的尺寸。Astro 的 layout 可以先產生一組預設值;遇到多欄卡片,仍要用符合版面的 sizes
覆蓋預設。

同一個 390px viewport,圖片可能只佔半欄,也可能撐滿內容區;同一個顯示寬度,在 DPR
2 的螢幕又需要更多像素。所以這件事不等於「手機放小圖、桌機放大圖」。要知道設定是否有效,最後得看瀏覽器的 currentSrc
和 runtime 回應,不能只看到 HTML 有 srcset 就收工。

先看固定 720px 少了什麼

Day 18:Astro Image/Picture 的基礎已把首頁三張能力卡交給
<Picture>,為每張來源圖產生 AVIF、WebP 與 PNG fallback:

<Picture
  src={image}
  formats={['avif', 'webp']}
  fallbackFormat="png"
  width={720}
  alt={imageAlt}
/>

格式選擇已經生效,但每種格式都只有一個 720px 寬版本。改動前的比較基準用 Headless Chrome 151、DPR 1,在 Cloudflare
adapter 的 astro preview 下量測 Day 10 能力卡:

viewport 圖片實際顯示寬度 currentSrc encoded body
1440px 363px 720×325 AVIF 8,411 B
390px 358px 720×325 AVIF 8,411 B

兩個 viewport 相差 1050px,圖片的 CSS 寬度卻只差 5px。原因在
Day 17:首頁骨架的 grid:桌面是三欄,窄螢幕才變成一欄。因此不能用 viewport 寬度代替圖片在版面預計佔用的寬度(下文簡稱 slot);sizes
描述的是後者。

瀏覽器怎麼用 srcsetsizes

以下是縮短後的輸出:

<img
  srcset="image-360.avif 360w, image-540.avif 540w, image-720.avif 720w"
  sizes="(max-width: 38rem) 92vw, 23rem"
  alt="文章列表由 Content Collection 產生"
/>

360ww 指候選檔案的固有寬度,不是 CSS 寬度。版面則由 sizes 描述:

  • viewport 不超過 38rem 時,圖片預計佔 92vw。
  • 其他情況預計佔 23rem。

瀏覽器先依 sizes 算出 slot 寬度,再把 DPR 納入候選評估。這次 Chrome 151、DPR 1 在 390px viewport 讀到的 92vw
約是 359px,因此選到 360w;桌面讀到 23rem,也就是約 368px,因此選到 540w。若同一個 390px viewport 改成 DPR
2,需求會接近 359 × 2,720w 才能覆蓋。規劃候選時要看 slot × 目標 DPR,不只看 CSS 顯示寬度。

<Picture> 同時處理格式與尺寸

首頁能力卡保留 Day 18 的格式 fallback,再加入 layout、候選寬度和符合現有 grid 的 sizes

---
const CARD_IMAGE_WIDTHS = [360, 540, 720];
const CARD_IMAGE_SIZES =
  '(max-width: 38rem) 92vw, (max-width: 52rem) 44vw, 23rem';
---

<Picture
  src={image}
  formats={['avif', 'webp']}
  fallbackFormat="png"
  layout="constrained"
  width={720}
  widths={CARD_IMAGE_WIDTHS}
  sizes={CARD_IMAGE_SIZES}
  alt={imageAlt}
  class="project-card__image"
/>

Build 後,一張能力卡有三層選擇:

標記 格式 寬度候選
第一個 <source> AVIF 360w、540w、720w
第二個 <source> WebP 360w、540w、720w
fallback <img> PNG 360w、540w、720w

瀏覽器先依 <source> 順序選支援的格式,再在該格式的 srcset 裡選尺寸。<Picture>
處理「同一張圖有哪些格式版本」,widthssizes 處理「每個格式有哪些寬度版本」。

CSS 仍負責圖片最後怎麼放進卡片:

.project-card__image {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

Astro 7.1.1 的 image.responsiveStyles 預設是 false。這個專案已有全域
img { max-width: 100%; height: auto; },卡片也有自己的 scoped
CSS,因此沒有再開自動樣式。若專案沒有自行處理圖片寬度,官方建議啟用
responsiveStyles;否則 HTML 雖然有候選,圖片未必會照預期縮放。Day 6:scoped 與 global CSS的分層在這裡也適用:全站只放溢出保護,卡片裁切留在元件內。

sizes 屬於版面,不屬於圖片檔

ArticleCard 同時出現在首頁多欄和第 3 頁的文章分頁。同一張 Day 17
cover,在首頁約寬 361px;到了分頁單欄,寬度約 702px。如果把首頁的 23rem 寫死在元件裡,分頁就會低報圖片真正佔用的空間。

版面資訊由呼叫端傳進元件:

<!-- 首頁:一欄、兩欄、三欄 -->
<ArticleCard ... imageSizes="(max-width: 38rem) 92vw, (max-width: 52rem) 44vw, 23rem" />

<!-- 分頁:閱讀區單欄 -->
<ArticleCard ... imageSizes="(max-width: 48rem) 92vw, 44rem" />

ArticleCard 只負責把這份資訊交給 <Image>。單欄分頁中的 ArticleCard 可能佔 44rem,因此候選上限也要涵蓋高 DPR 螢幕:

<image
  src="{cover.src}"
  layout="constrained"
  width="{720}"
  widths="{[360,"
  540,
  720,
  1080,
  1440]}
  sizes="{imageSizes}"
  alt="{cover.alt}"
  class="article-card__image"
/>

這個 props 邊界延續了
Day 5:Astro 元件與 props的原則:元件保留渲染規則,呼叫端提供所在版面的資料。實測同一張 cover 在首頁 1440px 選 540w、分頁 1440px 選 720w,來源圖與元件完全沒變,純粹是 slot 不同的結果。分頁單欄 44rem(約 704px)在 DPR 2 下需要約 1408px,因此 ArticleCard 候選保留到 1440w;而 ProjectCard 上限僅 23rem,720w 已足夠覆蓋 DPR 2 的顯示需求。

另一種常見做法:用裝置類別切兩套資產

除了用 sizes 描述 slot,也可以跳過 slot,直接依裝置類別準備兩套圖。

一個上線中的多語系品牌官網採用這個做法。public/
底下切成兩組目錄,一組給桌機、一組給手機,沿用「pc」與「h5」這組命名習慣。頁面則是原生 <img>

<img src="/images/pc/home/hero.png" alt="首頁主視覺" />

複製一份實測後,這個專案的圖片標記狀態如下:

檢查項 結果
astro:assets<Image><Picture> 未使用
srcsetsizes 未出現
widthheight 屬性 未設定
手機資產目錄被頁面引用 沒有;9 處圖片引用全部指向桌機路徑

兩套資產切好了,但沒有任何機制會依螢幕挑選,手機拿到的仍是桌機圖。專案另外有一個 client 端 plugin,監看視窗寬度並在
<html> 寫入 smmdlgxl
class;樣式檔裡沒有任何選擇器用到這些 class。裝置判斷函式庫也在依賴清單裡,程式碼沒有引用。

在沒有建置期圖片管線的年代,「設計交付兩套稿、前端依裝置載對應那套」是可行且直觀的流程:決策點只有一個布林值,也不需要理解候選與 DPR。代價是把「圖片要多大」綁在裝置類別上。同一個 390px
viewport,圖片可能撐滿內容區,也可能只佔半欄,裝置類別分不出這兩種。

<Picture>widthssizes
後,開發者只需描述 slot 與候選寬度,實際選擇交由瀏覽器與 DPR 決定。候選皆由單一來源圖產生,省去維護雙目錄的成本;而補齊明確的 widthheight 或 layout 屬性,也能在排查遺留專案時順便杜絕 Layout Shift。

layout 的預設值何時夠用

啟用 image.responsiveStyles,或自行提供等效 CSS 時,layout="constrained" 才能讓圖片隨容器縮小,並以指定的 width
作為最大顯示寬度。Astro 也會自動推導 srcset,以及這樣的 sizes

(min-width: 720px) 720px, 100vw

這份預設適合「窄螢幕時接近 viewport 全寬,寬螢幕時最多 720px」的圖片。首頁三欄卡片在 1440px
viewport 只佔約 363px,若沿用自動值,瀏覽器會以為它需要 720px。layout
只提供通用設定;多欄、sidebar、閱讀欄寬等版面資訊仍要由 sizes 說明。顯式傳入 widthssizes
後,自動推導不會參與這兩項設定;實際縮放則由前面列出的 global 與 scoped CSS 負責。

顯式 widths 也讓候選數量變成可檢查的效能預算。Astro 7.1.1 的自動候選會參考 layout、圖片寬度、原圖上限與 image service
breakpoints,這些細節可能隨版本或服務改變。首頁能力卡只需要 360、540、720 三個真實 slot 尺寸,直接列出更容易從 build
HTML 驗收。

什麼情況才需要 getImage()

目前兩個案例都在渲染語意圖片,<Image><Picture> 已經能產生正確標記。getImage()
是 server-only 的低階入口,讓程式先拿到最佳化結果,不是元件的進階替代品:

---
import { getImage } from 'astro:assets';
import image from '../assets/day-10/day10-collection-list.png';

const optimized = await getImage({
  src: image,
  format: 'avif',
  layout: 'constrained',
  width: 720,
  widths: [360, 540, 720],
  sizes: '(max-width: 38rem) 92vw, 23rem',
});
---

<img
  src="{optimized.src}"
  srcset="{optimized.srcSet.attribute}"
  {...optimized.attributes}
  alt="文章列表由 Content Collection 產生"
/>

回傳結果包含 srcsrcSetattributesoptionsrawOptions。自製圖片元件、CSS
background、metadata 或其他需要先取得 URL/attributes 的 server 流程,才需要下到這一層。若只是輸出
<img>,現成元件會處理 alt 契約和標記,程式碼也比較短。getImage() 不在瀏覽器現場轉檔;它仍透過 Astro 的 image
service 產生結果。

currentSrc 才回答瀏覽器最後選了什麼

套用多寬度候選後,以相同環境重新量測 Day 10 能力卡:

viewport 圖片 CSS 寬 currentSrc encoded body 相較固定 720px
1440px 363px 540×244 AVIF 5,264 B -37.4%
390px 358px 360×163 AVIF 2,613 B -68.9%

1440px 首頁維持三欄能力卡,圖片由 Picture 提供 AVIF、WebP 與多寬度候選

390px 首頁能力卡維持單欄,瀏覽器在 DPR 1 選到 360px AVIF 候選

這些百分比比較的是同一張 Day 10 來源、同為 AVIF 的 720/540/360px encodedBodySize,也就是編碼後 response
body,因此比 Day 18 的跨格式表更接近純尺寸差異。它仍是本機 adapter、特定瀏覽器與 DPR
1 的結果,不是所有裝置的固定節省率;若改看包含 response headers 的 transferSize,兩組降幅分別是 36.1% 與 66.6%。

驗證分成三層:

  1. Build HTML 證明 Astro 產生 360w/540w/720w 與 sizes
  2. 瀏覽器的 currentSrc 證明這次條件下最後選了哪一個候選。
  3. Cloudflare /_image runtime 回應的 Content-Type、decoded dimensions 與 Resource
    Timing,分別交叉確認格式、像素尺寸與 bytes。

只做第一層,會知道「可以選什麼」,卻不知道「最後選了什麼」;只看 currentSrc URL,又可能把尚未驗證的 query
string 當成實際轉檔結果。Day 16:分頁與轉址也用過相同的分層方式:設定、build 產物與真實 HTTP 行為各自提供不同證據。

候選不是越多越好

每增加一個 widths 候選,image service 就多一種可能的轉換;<Picture>
再乘上格式數量。預先產圖的服務可能增加 build 產物,on-demand endpoint 則增加可能的 runtime 轉換與 cache
key,不代表每個候選都會立刻產生。候選太少會讓瀏覽器被迫下載過大的圖,太多則提高轉換與維護成本。先從真實版面寬度與目標 DPR 抓三到五個區間,比抄一長串裝置尺寸更容易維持。

sizes 也是一份會過期的版面契約。grid breakpoint、page
gutter 或卡片最大寬度改動後,要一起重測。這次 1440px/390px 首頁都沒有水平溢出,本機首次載入未觀測到 Layout
Shift,首頁仍是 3 個 <picture>、6 個 <source> 與 0 個 <astro-island>。這只是本次觀測,不等同正式站的 Core Web
Vitals 報告。圖片候選發生在 HTML 與 image runtime,不需要增加
Day 14:搜尋 island那種 client hydration。

Astro 7.1.1 的現行規則可查
Images 指南astro:assets API
image.responsiveStyles 設定。圖片 API 與 image
service 細節容易變;持久的判斷方式是先量 CSS slot,再檢查 srcsetsizes,最後到瀏覽器與 runtime 驗證。

從版面 slot 決定 sizes,由瀏覽器依 DPR 選取候選,再透過 currentSrc 與 runtime 交叉驗證,就能在零額外 hydration 下確保各裝置下載合理尺寸。下一篇
Day 20:Endpoints 與 RSS改看資料輸出,繼續區分 build-time 產物與 request-time 回應。

本日程式碼:step-19|只看這天的改動:step-18...step-19


上一篇
圖片只用原生 img 夠嗎?Astro Image/Picture 處理了什麼?
下一篇
為什麼說 Astro 不只是靜態網站?用 endpoints 建立 RSS 與 JSON
系列文
用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言