網站圖片無縫兼容 WebP/AVIF

前言

WebP 格式發佈已有十餘年,但不少站點至今仍未使用,只爲兼顧極少數低版本瀏覽器。至於去年發佈的 AVIF 格式,使用的站點就更少了。

然而圖片往往是流量大戶,與其費盡心機優化腳本體積,可能還不如轉換一張大圖帶來的收益更多。據 caniuse 統計,如今有 67% 的用戶支持 AVIF、95% 的用戶支持 WebP。先進的格式觸手可得,卻因兼容性問題仍堅守 PNG、GIF 等古老格式,白白浪費網站流量,以及用戶加載時間,實在浪費。

事實上,對於同一個圖片 URL,完全可爲低版本瀏覽器使用老格式,爲高版本瀏覽器使用新格式,從而實現無縫兼容。本文講解後端和前端兩種不同的實現方案。

後端方案

這是最簡單也是最普及的方案,網上能搜到很多相關的文章。不過這其中存在諸多細節,大多數文章都未考慮全面。

原理

支持 WebP 的瀏覽器,HTTP 請求的 Accept 頭會包含 image/webp 字符,後端可根據該特徵返回 WebP 版圖片;不支持的瀏覽器則沒有該特徵,後端返回原始圖片。

AVIF 同理,特徵爲 image/avif。由於 AVIF 比 WebP 更先進,因此需優先判斷。

實現

由於圖片解碼和編碼開銷很大,因此格式轉換通常離線完成,例如預先將 foo.jpg 轉換成 foo.jpg.webpfoo.jpg.avif。這裏有幾個細節:

  • 如果 WebP 文件比原文件更大,那就沒有必要保留 WebP 文件。AVIF 同理

  • 如果原文件本身就是 WebP 文件,那就不用再轉 WebP 了。但可以嘗試轉 AVIF 版本,如果更小則保留

  • 如果原文件本身就是 AVIF 文件,那什麼都不用做

運行時的判斷邏輯很簡單,但也容易疏漏。以 nginx 爲例,通常會這樣配置:

http {
  map $http_accept $_ext {
    ~image/avif   .avif;
    ~image/webp   .webp;
    default       '';
  }
  server {
    location / {
      add_header  Vary  Accept;
      try_files   $uri$_ext  $uri  =404;
    }
  }
}

看起來好像沒問題,但遇到這種情況就不對了:用戶支持 WebP 和 AVIF,但後端只存在 WebP 文件。正常應該返回 WebP,但這裏 try_files 只嘗試一次,找不到 $uri.avif 就返回原文件了。顯然不對。

正確應該 try_files 兩次:

http {
  map $http_accept $_avif {
    ~image/avif           .avif;
    ~image/webp           .webp;
    default               '';
  }
  map $http_accept $_webp {
    ~image/webp           .webp;
    default               '';
  }
  server {
    location / {
      add_header  Vary  Accept;
      try_files   $uri$_avif  $uri$_webp  $uri  =404;
    }
  }
}

注意這裏的邏輯順序。即使用戶不支持 AVIF 擴展名也不能直接返回空,否則就是在嘗試原文件了。至於可能會重複嘗試兩次 WebP 文件,雖不優雅但也無大礙。

此外,如果希望用戶訪問目錄名時 URL 末尾能自動添加 /(例如訪問 /blogs 時先重定向到 /blogs/),那麼 try_files 還需添加 $uri/

演示

訪問:https://www.etherdream.com/img-test/fox.html

支持 AVIF 的瀏覽器,返回的圖片類型爲 image/avif

不支持 AVIF 但支持 WebP 的瀏覽器,返回的圖片類型爲 image/webp

既不支持 AVIF 又不支持 WebP 的瀏覽器,返回的圖片類型爲 image/jpeg

優點

後端實現的方案顯然通用性很好,前端無需修改即可生效,甚至前端不是瀏覽器都沒關係,只要遵循 HTTP 的 Accept 規範即可。

缺點 1

由於同一個 URL 會返回不同的內容,如需通過 CDN 加速,則需配置 Vary: Accept 響應頭,以確保代理服務器能根據不同的 Accept 請求頭緩存相應的內容。然而目前 CloudFlare 免費版卻無視 Vary,開啓這個功能意味低版本瀏覽器顯示不了圖片!

缺點 2

不同格式的圖片,即使像素完全相同,但文件數據顯然是不同的。假如業務依賴文件數據,例如校驗文件 Hash,那麼顯然會失敗,從而導致業務損壞。

對於這個問題,有兩種緩解方案:

  1. 判斷 Fetch Metadata 相關的請求頭,對於有能力讀取文件數據的請求,則不考慮升級

  2. 通過黑白名單機制,只允許或不允許某些圖片升級

第 1 種方案更通用,但 Fetch Metadata 只有較高版本的瀏覽器才支持,並且某些特殊場合仍可能存在問題。第 2 種方案更穩定,但需整理文件列表並在後端維護,顯然很麻煩。

前端方案

如果網站搭建在虛擬空間、GitHub Pages 等這類無法修改配置的後端,或者使用了 CloudFlare 免費加速服務,那隻能在前端實現。

原理

前端升級圖片有多種方案。最容易想到的就是用 JS 在線解碼高版本圖片。當初 WebP 發佈時我對此頗有興趣,嘗試用 Flash 實現 WebP 解碼器,並且能自動替換網頁中的圖片元素,看起來就像原生支持一樣。但實際應用後發現並不理想,一是不支持 CSS 圖片(實現很麻煩),二是解碼性能差。雖然使用了 Alchemy 編譯技術(LLVM → ActionScript ByteCode),但性能相比原生仍差一大截。最終放棄了這個方案。

儘管後來有更先進的計算方案,例如 WebWorker、asm.js / WebAssembly、SIMD 等,但仍然達不到原生性能,並且代碼體積很大。所以在線解碼的方案仍不考慮。

直到另一個黑科技的出現,使得前端升級圖片變得非常容易,並能覆蓋網頁中所有圖片,那就是 Service Worker。它能攔截當前站點產生的所有請求,並能控制返回結果,相當於一個反向代理服務。於是我們可以在 Service Worker 中判斷 Accept 請求頭,然後代理到相應的 URL。

實現

得益於 Service Worker 強大的功能,圖片除了格式升級外還能玩出很多「騷操作」,例如可將圖片部署在免費的圖牀、相冊上,使用時根據清單中的地址進行反向代理,從而可將圖片流量降低到 0!並且可準備多個圖片 URL 做冗餘備份,以及完整性校驗等等。這個思路之前在 網站 CDN 去中心化 嘗試過,不過實際應用起來似乎並不容易。

最近我重新整理這個思路,並實現了一個工具:freecdn,它可以自動生成清單文件,記錄原文件的備用 URL 列表、Hash 值、是否支持 WebP/AVIF 升級等信息。

演示

訪問:https://freecdn.etherdream.com/fox.html

Service Worker 不僅將 JPEG 升級成 AVIF 版本,甚至從免費 CDN 加載,將流量開銷「優化」到了零!

還有更有趣的現象 —— 新建一個隱身窗口,打開控制檯網絡欄,訪問:https://freecdn.etherdream.com/fox.jpg

從瀏覽器界面上看,和直接訪問圖片一模一樣,但實際上該圖片是由 Service Worker 提供的。具體原理和細節可 查看這裏

優點

後端則無需任何修改,無論是普通的服務器、CDN 還是虛擬空間都可以。前端只需引用一個腳本開啓 Service Worker,無需修改業務邏輯。

由於 Service Worker 運行在前端,因此能獲取到更詳細的 請求上下文信息,從而可實現更智能的策略。此外,即使要配置黑白名單,只需通過一個清單文件即可實現,比修改後端服務配置方便很多。

缺點

如果用戶的瀏覽器不支持腳本,或者根本不是瀏覽器訪問,那麼 Service Worker 顯然無法運行,圖片升級功能自然就失效了。這種情況只能使用後端方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章