<Picture> 會先依 <source> 順序與瀏覽器支援度選格式,再由該格式的 srcset、sizes
與裝置像素比(DPR)決定實際下載的尺寸。Astro 的 layout 可以先產生一組預設值;遇到多欄卡片,仍要用符合版面的 sizes
覆蓋預設。
同一個 390px viewport,圖片可能只佔半欄,也可能撐滿內容區;同一個顯示寬度,在 DPR
2 的螢幕又需要更多像素。所以這件事不等於「手機放小圖、桌機放大圖」。要知道設定是否有效,最後得看瀏覽器的 currentSrc
和 runtime 回應,不能只看到 HTML 有 srcset 就收工。
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
描述的是後者。
srcset 和 sizes以下是縮短後的輸出:
<img
srcset="image-360.avif 360w, image-540.avif 540w, image-720.avif 720w"
sizes="(max-width: 38rem) 92vw, 23rem"
alt="文章列表由 Content Collection 產生"
/>
360w 的 w 指候選檔案的固有寬度,不是 CSS 寬度。版面則由 sizes 描述:
瀏覽器先依 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>
處理「同一張圖有哪些格式版本」,widths/sizes 處理「每個格式有哪些寬度版本」。
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> |
未使用 |
srcset、sizes |
未出現 |
width/height 屬性 |
未設定 |
| 手機資產目錄被頁面引用 | 沒有;9 處圖片引用全部指向桌機路徑 |
兩套資產切好了,但沒有任何機制會依螢幕挑選,手機拿到的仍是桌機圖。專案另外有一個 client 端 plugin,監看視窗寬度並在<html> 寫入 sm/md/lg/xl
class;樣式檔裡沒有任何選擇器用到這些 class。裝置判斷函式庫也在依賴清單裡,程式碼沒有引用。
在沒有建置期圖片管線的年代,「設計交付兩套稿、前端依裝置載對應那套」是可行且直觀的流程:決策點只有一個布林值,也不需要理解候選與 DPR。代價是把「圖片要多大」綁在裝置類別上。同一個 390px
viewport,圖片可能撐滿內容區,也可能只佔半欄,裝置類別分不出這兩種。
<Picture> 配 widths、sizes
後,開發者只需描述 slot 與候選寬度,實際選擇交由瀏覽器與 DPR 決定。候選皆由單一來源圖產生,省去維護雙目錄的成本;而補齊明確的 width/height 或 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 說明。顯式傳入 widths 與 sizes
後,自動推導不會參與這兩項設定;實際縮放則由前面列出的 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 產生"
/>
回傳結果包含 src、srcSet、attributes、options 與 rawOptions。自製圖片元件、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% |


這些百分比比較的是同一張 Day 10 來源、同為 AVIF 的 720/540/360px encodedBodySize,也就是編碼後 response
body,因此比 Day 18 的跨格式表更接近純尺寸差異。它仍是本機 adapter、特定瀏覽器與 DPR
1 的結果,不是所有裝置的固定節省率;若改看包含 response headers 的 transferSize,兩組降幅分別是 36.1% 與 66.6%。
驗證分成三層:
sizes。currentSrc 證明這次條件下最後選了哪一個候選。/_image runtime 回應的 Content-Type、decoded dimensions 與 Resource只做第一層,會知道「可以選什麼」,卻不知道「最後選了什麼」;只看 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,再檢查 srcset/sizes,最後到瀏覽器與 runtime 驗證。
從版面 slot 決定 sizes,由瀏覽器依 DPR 選取候選,再透過 currentSrc 與 runtime 交叉驗證,就能在零額外 hydration 下確保各裝置下載合理尺寸。下一篇
Day 20:Endpoints 與 RSS改看資料輸出,繼續區分 build-time 產物與 request-time 回應。
本日程式碼:step-19|只看這天的改動:step-18...step-19