網站圖片無縫兼容 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.webp
和 foo.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/
。
演示
訪問://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,那麼顯然會失敗,從而導致業務損壞。
對於這個問題,有兩種緩解方案:
-
判斷 Fetch Metadata 相關的請求頭,對於有能力讀取文件數據的請求,則不考慮升級
-
通過黑白名單機制,只允許或不允許某些圖片升級
第 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 升級等資訊。
演示
訪問://freecdn.etherdream.com/fox.html
Service Worker 不僅將 JPEG 升級成 AVIF 版本,甚至從免費 CDN 載入,將流量開銷「優化」到了零!
還有更有趣的現象 —— 新建一個隱身窗口,打開控制台網路欄,訪問://freecdn.etherdream.com/fox.jpg
從瀏覽器介面上看,和直接訪問圖片一模一樣,但實際上該圖片是由 Service Worker 提供的。具體原理和細節可 查看這裡。
優點
後端則無需任何修改,無論是普通的伺服器、CDN 還是虛擬空間都可以。前端只需引用一個腳本開啟 Service Worker,無需修改業務邏輯。
由於 Service Worker 運行在前端,因此能獲取到更詳細的 請求上下文資訊,從而可實現更智慧的策略。此外,即使要配置黑白名單,只需通過一個清單文件即可實現,比修改後端服務配置方便很多。
缺點
如果用戶的瀏覽器不支援腳本,或者根本不是瀏覽器訪問,那麼 Service Worker 顯然無法運行,圖片升級功能自然就失效了。這種情況只能使用後端方案。