· 6 分鐘閱讀

用 Cloudflare Images 當作圖片儲存、轉檔方案

# 開發筆記

在網頁上放一張圖片,大概是前端最簡單的一件事:

<img src="YOUR_IMAGE_URL" />

一行就搞定。簡單到會讓人忘記這行 src 後面其實藏了很多細節。

圖片是網頁裡最重的東西。文字才幾 KB,一張沒處理過的照片動輒好幾 MB。使用者覺得快不快,很多時候不是卡在你的 JavaScript,而是卡在圖片載入的那幾秒。

要省位元組,第一步是壓縮。JPEG 用離散餘弦轉換把人眼不敏感的高頻細節丟掉,換來檔案大幅縮小,雖然是有損壓縮,但視覺上幾乎看不出差別。

近年的 WebP、AVIF 又把壓縮率往前推了一大截,代價是有些舊瀏覽器不支援,所以要用 <picture> 準備 fallback:

<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" />
</picture>

瀏覽器會由上往下挑第一個支援的格式來渲染。width/heightsrcsetsizes、art direction——這一塊我在另一篇文章寫得比較完整,這裡不重複。

這篇想講的是它的另一半:這些不同格式、不同尺寸的圖片,到底是從哪裡來的?

依照使用情景來說,為了在網頁上有更好的體驗,通常會對圖片做以下處理:

  • 縮成列表用的 thumbnail、內頁用的中圖、大圖
  • 每種尺寸再各自生一份 AVIF、WebP、JPEG
  • 依裝置的螢幕寬度跟 DPR 決定實際送哪一張,或是在前端動態選擇
  • 把這些檔案放進 storage,前面再接一層 CDN 快取

如果是固定數量的靜態檔案,我們大可以在 build time 時期事先將圖片編譯成各種需要的格式,但是在 UGC 的平台上並沒辦法控制數量,都是由使用者上傳後在伺服器端處理。

這些處理每一步拆開都不難,難的是做完整還要撐得住流量,這兩件事湊在一起,成本會高得不成比例。問題的核心是:圖片轉檔是 CPU-bound 的工作。

以 AVIF 舉例好了,AVIF 是從 AV1 影片編碼的關鍵影格衍生出來的格式1,而影片編碼天生就是「編碼很貴、解碼很便宜」,它刻意把 CPU 成本壓在編碼端,好讓播放時解碼順暢。

AVIF 檔案很小、瀏覽器解碼很快,而且品質相對好很多,但把一張圖壓成 AVIF 非常吃 CPU。Jake Archibald 實測過,用 libavif 最高的 effort 參數,光壓一張圖就可能花上十分鐘1。你當然不會真的開到那麼高,但這個數字說明了它的量級——AVIF 編碼比 JPEG 貴上好幾倍,是規格設計使然。

於是「在自己伺服器上動態轉圖」這個看起來最直覺的做法就變得危險。圖片一多、又剛好撞上流量尖峰,這些轉檔工作會直接把 CPU 吃滿,排擠掉其他商業邏輯的程式碼,甚至還更容易出現 OOM。

像 Next.js 的 next/image,預設就是用伺服器資源做 on-demand 轉檔,背後跑的正是 Sharp。方便是真的方便,但它等於把一個吃 CPU、又對外開放的端點塞進了你的應用程式裡。

官方文件自己都要你設好 qualitiesremotePatterns 白名單,否則會變成被人拿來大量轉圖、灌爆記憶體的破口2。我個人不喜歡框架在這一層插手,多了一層我不好掌控的複雜度。

**面對高流量系統,任何 CPU-bound 且需要吃掉大量記憶體的任務,都要非常小心。**一旦流量變大,伺服器很容易就遇到瓶頸,甚至影響到其他業務邏輯的程式碼。

那麼透過 Lambda 或是非同步的方式將圖片處理 offload 到別的地方呢?可以,但這樣一來你要處理的複雜度就會指數上升。

AWS 官方自己包了一套方案(以前叫 Serverless Image Handler,現在改名 Dynamic Image Transformation for Amazon CloudFront)3,光看它的組成就知道水有多深:CloudFront 當快取層、API Gateway 當入口、Lambda 跑 Sharp 做轉檔、S3 放原圖跟 log,想要智慧裁切還得接 Amazon Rekognition,想擋盜連還得用 Secrets Manager 做簽章。這裡面每一個都是要設定、要維護、要付錢的東西。

還有一個容易被忽略的成本:頻寬。圖片是流量大戶,而雲端的對外傳輸(egress)是要另外收錢的。

直接從 S3 對外送圖又特別貴,標準做法是前面墊一層 CloudFront,靠快取把打到 origin 的請求擋下來。但這裡藏了一個陷阱:你切出來的衍生檔案越多(尺寸 × 格式),快取就越分散,一旦沒命中,就得回頭讀 S3、再轉一次、再送一次——等於同一份流量的錢付了兩遍。你為了省頻寬做的多格式多尺寸,反過來稀釋了快取的效益。

所以這件事的難處,不在任何單一步驟,而在於它是一條需要長期維護、又會隨流量放大成本的產線。除非公司規模已經可以負荷開發成本,且有足夠商業戰略意義,否則這種東西最好的策略就是不要自己做。

Cloudflare Images 幫你做完了

只要有圖片處理的需求,我現在基本上一律用 Cloudflare Images。前面講的那些事情,它都幫你搞定了。其他類似的服務還有 BunnyCDNImageKit 等等。

原圖只要傳一份上去。要不同尺寸、不同格式,不用預先切好幾十個檔案,而是在網址上帶參數,由 Cloudflare 在邊緣節點即時轉出來

打開 flexible variants 之後,網址長這樣4

https://imagedelivery.net/<account_hash>/<image_id>/w=400,quality=80

格式協商是自動的。走它的交付網址時,Cloudflare 會讀瀏覽器送來的 Accept header,支援 AVIF 就給 AVIF、只支援 WebP 就給 WebP,都不支援才 fallback 回原格式5——你不用自己寫 <picture> 去列格式,也不用自己判斷。

舉例來說,如果你打開瀏覽器複製這張圖的網址連結(https://image.kalan.dev/b856ee69-6431-48ed-7f22-3311e7d01600/normal),並觀察 Network,會發現回傳的格式是 AVIF,或是 webp,根據你的瀏覽器支援而定,但其實這張圖從頭到尾就只有一張原圖,我完全沒有做任何轉檔。

常見的操作也都內建:縮放、裁切(含依人臉裁切)、模糊、鏡像、旋轉、調亮度對比。

當然也可以掛自己的 domain。我的圖都放在 image.kalan.dev,只要那個 domain 是掛在同一個 Cloudflare 帳號底下的 zone 就行6

CDN 快取是預設的。轉好的圖直接進 Cloudflare 的邊緣快取,同一組(原圖 + 參數)第二次以後就從最近的節點回,不會再打到 origin。

它其實有兩種用法:一種是把圖直接存在 Cloudflare(Hosted Images),另一種是圖留在你自己的 storage(R2、S3 都行),只借用它邊緣的轉檔能力(現在叫 Transformations)。

收費也對應這兩種。存在它那邊會多算儲存跟交付的量,只借轉檔就只算轉檔次數,而且有免費額度7。目前收費如下:

MetricPricing
Images TransformedFirst 5,000 unique transformations included + $0.50 / 1,000 unique transformations / month
Images Stored$5 / 100,000 images stored / month
Images Delivered$1 / 100,000 images delivered / month

另外我也有寫了簡單的 cli,方便在 local 上傳圖片到 cloudflare images,如果有需要的話可以參考

Footnotes

  1. AVIF has landed — Jake Archibald。AVIF 衍生自 AV1 的關鍵影格;文中實測 libavif 最高 effort 壓一張圖可能超過十分鐘。 2

  2. next/image 官方文件,關於 on-demand 最佳化、記憶體上限與 qualities/remotePatterns 白名單。

  3. Dynamic Image Transformation for Amazon CloudFront(前身為 Serverless Image Handler)。

  4. Enable flexible variants — Cloudflare Docs

  5. Transform via URL — Cloudflare Docs

  6. Serve images from custom domains — Cloudflare Docs

  7. Cloudflare Images pricing。實際免費額度與計價以官方頁面為準。

相關文章

探索其他主題