Jimmy 的架站筆記

嗨~我是 Jimmy!我可能不是你認識的第 1 個 Jimmy,但一定是最帥的那個。


網站圖片的救星 - Cloudflare Images & Cloudflare Polish

By Jimmy 2024-09-01
發表於 devops
網站圖片的救星 - Cloudflare Images & Cloudflare Polish

不知道大家是不是跟我有一樣的問題,在處理網站的圖片時,希望可以根據使用者瀏覽器支援圖片的檔案種類來回傳支援度最高的圖片格式,ex: 使用者使用最新版本的 chrome 瀏覽器,最高支援的圖片檔案格式為 avif 檔,那我就希望 server 可以回傳圖片的 avif 檔給使用者。之前在閱讀 Web前端效能優化大補帖 這本書才發現,HTML 的 picture tag 可以支援不同的圖片格式,當瀏覽器不支援指定的圖片格式時,就會自動忽略該 source:

<picture>
  <source srcset="logo.webp" type="image/webp">
  <img src="logo.png">
</picture>

不過這樣一來,身為開發者還是需要先上傳同一張圖片不同的圖片格式,我自己也用這樣的方式處理過,缺點是每次有新功能需要在站上顯示新圖片時,自己就必須將設計師給的原檔(通常是 png 或 jpg)轉成 webp 以及 avif 檔,才能讓使用者吃到對應的圖片格式,而其他工程師一但沒有做轉檔的話,所有的使用者就只會吃到圖片的原檔。

這種處理方式還有一種缺點,那就是只能用在網站的靜態圖片資源,如果是站上使用者自己上傳的圖片(ex: 使用者的 avatar、文章的 banner 等等),因為不可能叫使用者上傳同一張圖片的不同檔案格式,所以就需要在後端多做一層處理,使用者上傳圖片後,後端需要將圖片轉成不同格式再上傳到 OSS(Object Storage Service),一方面開發成本變高,另一方面儲存成本也會變高。

因此其實一直都有在研究有沒有相關的服務可以處理這種需求:根據使用者的瀏覽器對圖片的支援度,來回傳最高支援度的圖片檔案格式,而開發者只要上傳圖片的原始檔即可,上禮拜終於找到了!!

Cloudflare 有兩個支援這個功能的服務:

1. Cloudflare Polish

Cloudflare polish 是 Cloudflare 提供自動最佳化圖片的 service,只要你的 domain 是 pro 以上的方案,就可以直接啟用這個 service,不需要做任何額外的設定,cloudflare 就會為你的圖片做最佳化,根據官方文件所述,會用以下方式來最佳化圖片:

Cloudflare 的計價是以 domain 為單位,pro 方案為 20 USD / 月 per domain

如何啟用 Cloudflare Polish 可以參考官方文件:Activate Polish

2. Cloudflare Images

Cloudflare Images 是 cloudflare 提供用來處理圖片相關的產品,你可以透過這個產品上傳圖片、transform 圖片(resize、指定回傳的圖片格式等等),個人覺得最實用的是其中的 transformations,開發者只需要上傳圖片的原始檔(你的圖片不一定要用 cloudflare 上傳,只要圖片的 source url 是在你 cloudflare 的 domain 底下就好,上傳到 S3 也沒問題),再透過這個服務,cloudflare 就可以自動根據 user 傳過來 request 的 accept header 來回傳最高支援度的檔案格式,接下來就說明一下如何使用這個服務:

2.1 啟用 Cloudflare Images

第一個步驟必須先訂閱這項服務:登入 cloudflare 後進到 dashboard,點擊 image tab

這時候會顯示 cloudflare images 的 pricing plan,基本上免費方案就可以使用 cloudflare image transformation 了,因此點擊免費方案即可

接著 cloudflare 會請你確認剛剛點擊的方案包含的服務,直接點擊下一步就好

再來會請你確認是否要上傳圖片到 cloudflare image,大部分的人應該都會把圖片上傳到 OSS - Object Storage Service(ex: AWS S3、Google Cloud Storage 等等),因此這個步驟點擊「Use my own storage」就好。

最後一步則是確認價格,金額是 $1.0 USD / 2000 images,不過如果是單純使用上述提到的功能(自動根據使用者的 accept header 回傳對應的檔案格式,也就是 format option)則不算在裡面。

2.2 啟用 Cloudflare Image Transformations

Transformations 是 Cloudflare Images 底下的其中一個服務,可以對圖片做 resize、格式轉換、壓縮、blur(模糊化)等等。

在 sidebar 的 images tab 底下點擊「Transformations」,接著選擇想要啟用 transformations 的 domain:

接著會詢問你是否要在這個 domain 啟用這項服務,直接點擊「Enable for zone」:

這樣就可以成功在這個 domain 啟用這項服務了!

2.3 Transformations 使用方式

要為你網站的圖片使用 transformations 有兩種方式:url 以及 cloudflare worker

2.4 Transform via URL

第一種方式是直接用 cloudflare 定義的 url 來使用 transform:

https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>

接著我們來看一下範例:

先隨便找一張剛剛啟用 domain 網站的圖片,開啟 dev tool 後,切換到 network tab,重新整理頁面(才會 record requests),點擊 Filter 的 Img,把圖片的 requests 篩選出來,接著找一張你希望測試的圖片,點擊右鍵後再把這張圖片的 request 以 curl 的方式 copy 下來:

接著到 Postman(或是任何可以打 request 的軟體或 library)點擊 import 後,直接貼上剛剛 copy as curl 的字串:

點擊 send 後,我們可以拿到回傳回來的圖片檔以及 response headers,可以看到這個圖片的格式是 jpg 檔,response 大小是 194.72 KB(response 大小為 body(以這個 request 來說就是圖片大小)加上 headers 大小):

接著我們照剛剛 Cloudflare 官方文件所提供的 url 做圖片的 transformations,把 url 改成:

https://take-a-rest.co/cdn-cgi/image/format=auto/https://take-a-rest.co/wp-content/uploads/2021/10/%E5%B1%B1%E7%94%B7Yamasan-40.jpg

這時候再發 request 會發現:竟然回傳 avif 檔了!!因為剛剛是直接從瀏覽器把 request 複製下來,瀏覽器發 request 會帶上支援什麼格式的 accept header,我的瀏覽器最高支援到 avif 檔,所以 cloudflare 就會幫我把我的原始檔轉成 avif 檔,接著就直接回傳,可以看到 response size 直接變成 136 KB,在只更動 url 的情況下,就可以讓圖片的 request 減少 30% 的大小

第一次對 image transform 時,因為 cloudflare 需要時間轉換,所以 response time 會比較久,圖片越大需要 transform 的時間也會越長,不過 transform 過後 cloudflare 就會把圖片 cache 起來,之後發同樣的 request 就會直接 cache hit 不需要再做 transform

統整一下這個 request 的資訊

再發一次相同的 request 就可以發現 response time 大幅減少,且 CF-Cache-Status(cloudflare 用來記錄 cache 的 header) 的 header 為 HIT

使用 URL 來 transform 的好處是,只需要更換站上的圖片 url 就可以使用這個服務,不過壞處也很顯而易見,使用者可以直接用 url 來轉換你的圖片,也無法隱藏圖片的 source url。

2.5 Transform via Workers

由於透過 url 會暴露圖片的 source url,同時也可能被惡意使用者使用圖片轉換的功能,因此 cloudflare 還支援另外一種方式來做圖片的 transformation - Cloudflare worker。

Cloudflare worker 是 cloudflare 的 serverless 服務,有點像是 AWS 的 lambda,基本上免費額度給的 quota 就已經很夠用了,每天可以發 100,000 個 requests:

用 worker 的好處如下

要用 worker 來作 image transformations 的話,第一步必須先建立 worker,點擊 sidebar 的 Workers & Pages:

接著會顯示建立 worker 的頁面以及一些 Cloudflare 提供的 templates 和 examples,直接點擊「Create Worker 」就好

可以自定義 worker 的名稱,不過因為之後會綁定某個 subdomain,所以這裡可以先忽略。直接點擊「Deploy」,之後才可以編輯 worker 的 code

成功 deploy 後,點擊「Edit code」可以開始編輯這個 worker 的 code

Cloudflare 官方文件有提供了 example worker 可以參考,我把他稍微調整一下:

/**
 * Welcome to Cloudflare Workers! This is your first worker.
 *
 * - Run "npm run dev" in your terminal to start a development server
 * - Open a browser tab at <http://localhost:8787/> to see your worker in action
 * - Run "npm run deploy" to publish your worker
 *
 * Learn more at <https://developers.cloudflare.com/workers/>
 */

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Fetch and log a request
 * @param {Request} request
 */
async function handleRequest(request) {
  // Parse request URL to get access to query string
  let url = new URL(request.url)

  // 用 worker 處理 image 時,options 會放在 cf object 裡
  let options = { cf: { image: {} } }

  // 這裡可以用來處理這個 worker 希望支援的 options
  if (url.searchParams.has("fit")) options.cf.image.fit = url.searchParams.get("fit")
  if (url.searchParams.has("width")) options.cf.image.width = url.searchParams.get("width")
  if (url.searchParams.has("height")) options.cf.image.height = url.searchParams.get("height")
  if (url.searchParams.has("quality")) options.cf.image.quality = url.searchParams.get("quality")

  // 用 worker 作 transformation 的話,format option 就不支援 auto 了
  // 需要再額外判斷 reqeust 的 accept header
  const accept = request.headers.get("Accept");
  if (/image\\/avif/.test(accept)) {
    options.cf.image.format = 'avif';
  } else if (/image\\/webp/.test(accept)) {
    options.cf.image.format = 'webp';
  }

  // 用來取得 image 的 source url
  // 如果你的 image url 是有規律的(ex: 放在 S3 的某個 bucket)
  // 那也可以只讓 client 帶上 object key 就好,這樣就能避免 client 拿到 image 的 source url
  const imageURL = url.searchParams.get("image")
  if (!imageURL) return new Response('Missing "image" value', { status: 400 })

  try {
    // TODO: Customize validation logic
    const { hostname, pathname } = new URL(imageURL)

    // Cloudflare images 只支援這些檔案格式,因此如果 request 的檔案格式不在其中的話,直接回傳 400 Error
    // @see <https://developers.cloudflare.com/images/url-format#supported-formats-and-limitations>
    if (!/\\.(jpe?g|png|gif|webp)$/i.test(pathname)) {
      return new Response('Disallowed file extension', { status: 400 })
    }

    // 只處理特定 domain 的 requests
    if (hostname !== 'take-a-rest.co') {
      return new Response('Must use "take-a-rest.co" source images', { status: 403 })
    }
  } catch (err) {
    return new Response('Invalid "image" value', { status: 400 })
  }

  // Build a request that passes through request headers
  const imageRequest = new Request(imageURL, {
    headers: request.headers
  })

  // Returning fetch() with resizing options will pass through response with the resized image.
  return fetch(imageRequest, options)
}

調整完 code 後,可以直接更新右側的 url,在最後面加上 ?image=[image_source_url] 測試一下 worker 是否有正確處理

更新 url 後按下 send 如果有正確拿到圖片的 response 那就代表成功了哦!確認成功後別忘了要 deploy 才會更新 worker 的 code。

然而你會發現,deploy 後即便我的 request accept header 有 image/avif,卻不會回傳 avif 檔,而是圖片原始的 jpg 檔

這主要是因為這個 worker 並不在我們剛剛 enable 的 domain 裡面,要解決這個問題,需要調整這個 worker 的設定:點擊這個 worker 後,進到 Settings → Triggers → Add Custom Domain

輸入你希望觸發這個 worker 的 subdomain,舉例來說:我的 domain 是 take-a-rest,我就可以將觸發這個 worker 的 domain 設為 image.take-a-rest.co ,確定好 domain 後就可以點擊「Add Custom Domain」。

這時候會發現剛剛輸入的 domain 的 Certificate 欄位還在 Initializing,不過通常幾分鐘後 Cloudflare 就會處理好變為 Active 的狀態,一旦 Activate 後,我們就可以將剛剛 worker 的 domain 改成剛剛設定的 custom domain 來測試看看

可以看到把 url 換成剛剛設定的 custom domain 後,Cloudflare 就會正確幫我們回傳 avif 檔了!

結論

上週發現這個服務的時候超驚喜,只需要上傳圖片的原始檔 Cloudflare 就會自動轉檔並作 cache,而且還免費,真的太佛了!希望這篇文章有幫助到有類似需求的開發者,或是大家知道有什麼類似的服務,也歡迎留言分享給我,謝謝!


你可能也會喜歡

Linux Node.js 的升級與安裝

Linux Node.js 的升級與安裝

最近在處理公司產品的套件升級,發現滿多套件都依賴 Node.js,需要先升級機器的 Node.js 才能進行後續的處理,這篇文章就以 Ubuntu 為例,筆記一下在 Linux 中升級 Node.js 的方法。 如果擔心直接升級機器的 Node.js 的話,我滿推薦先用在自己的電腦開 Linux 的 VM 來做一些測試。

Read More