動態 Favicon 實戰:讓瀏覽器分頁動起來(含可執行 Demo)
讀這篇文章的時候,你大概已經注意到這個分頁上頭有東西在動了。這就是動態 favicon —— Gmail 用它顯示新郵件數,Discord 用它顯示通知紅點,原理一樣,能玩出什麼花樣幾乎只看你能在 canvas 上畫什麼。
這篇文章嵌了一個 demo,你可以直接玩。按一個按鈕,看看分頁變了沒。沒有截圖、沒有內嵌影片 —— 你現在看到的這個 favicon 就是 demo 本身。
為什麼要做動態 Favicon?
老實說,大多數網站不該做。每個分頁裡都在轉的圖示用沒多久就煩人,還吃 CPU。但下面幾種情境真的值得:
- 載入或處理狀態。 長時間的上傳、匯出、建置。使用者切到別的分頁等結果,動態 favicon 能讓他知道事情還在跑。
- 通知紅點。 新訊息、@提醒、警示。會輕輕脈動的紅點比靜態的更容易被注意到。
- 即時資料流。 交易看板、監控工具、比賽比分 —— 標題列不夠用的時候。
- 品牌時刻。 節日小特效、上線紀念日。點到為止。
如果你的場景不在上面這些,就別搞動畫了。乾乾淨淨的靜態 SVG favicon 在檔案大小、深色模式、電池續航上都比較划算。
即時 Demo:現在就試
挑一個動畫,然後看看你瀏覽器分頁 —— 上頭那個小圖示正在被重畫。
真正的 favicon 只有 16×16 像素,太小看不出細節,所以左邊這個框用最近鄰縮放放大 8 倍同步顯示同一張 canvas。
狀態:閒置
整套動畫迴圈現在完全跑在 Web Worker 裡,做法和 Aymkdn 函式庫的 favicon_worker.js 一樣。每隔 20ms(50fps),worker 在自己的 OffscreenCanvas 上畫一幀,透過 convertToBlob + FileReader 匯出成 data URL,再把字串送回主執行緒。主執行緒只做一件事:把那串字串指派給 faviconLink.href。這就是為什麼圖示動得跟 GitHub 的 demo 一樣順。
想看這套方案在真實產品裡跑起來是什麼樣?Random Picker Wheel 就用動態 favicon 同步它的轉盤——轉盤式 UI 正是動畫圖示真正貼合產品的少數場景之一。打開頁面,轉一下轉盤,看看分頁標籤上的圖示也跟著一起轉。
它到底是怎麼動起來的
整個技術只有三步:
// 1. 取得或新建一個 favicon 的 link 元素
let link = document.querySelector('link[rel~="icon"]');
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
// 2. 在隱藏 canvas 上畫一幀
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
function drawFrame(t) {
const scale = 0.5 + 0.5 * Math.abs(Math.sin(t / 400));
ctx.clearRect(0, 0, 32, 32);
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.arc(16, 16, 14 * scale, 0, Math.PI * 2);
ctx.fill();
// 3. 把 canvas 匯出成 data URL,指派給 favicon
link.href = canvas.toDataURL('image/png');
requestAnimationFrame(drawFrame);
}
requestAnimationFrame(drawFrame);
差不多 15 行就能做一個會脈動的紅點。再花俏的東西,都只是在 canvas 上畫不一樣的圖形而已。
瀏覽器相容性的真實情況
這部分開始有點難看。各家瀏覽器更新 favicon 的勤勞程度差很多:
- Firefox:分頁不在前景也照樣播放順暢,業界標竿。
- Chrome / Edge:分頁活躍時播放正常。一切走,
requestAnimationFrame就被節流到大約每秒一次,動畫會變慢甚至卡住。 - Safari:分頁在前景時能動,但更新間隔有時會拉很長,不要期待流暢。
但對最常見的用法 —— 通知紅點、進度狀態 —— 這其實不是問題,本來也不需要每秒更新好幾次。60fps 的順滑旋轉很多時候只是裝飾。
後台分頁的 Web Worker 技巧
上面的「GitHub 風格翻轉」按鈕,是把 Aymkdn/animated-favicon 這個函式庫的招牌動畫原樣搬過來:圖示 A 停 3 秒,按 cosine 把寬度壓到零,切換成圖示 B,再展開回去。背後的算式直接出自函式庫裡的 favicon_worker.js:width = canvas.width * Math.abs(Math.cos(progress * Math.PI)),當 progress 越過 0.5,第二張圖開始接手。
Aymkdn 的函式庫還多走了一步:把這整個迴圈跑在 Web Worker 裡。Worker 不會因為分頁切到後台而被節流,動畫照樣在跑,再加上 OffscreenCanvas,整個渲染過程根本不需要碰 DOM。
程式結構大致是這樣:
// 主執行緒
const worker = new Worker('favicon-worker.js');
worker.onmessage = (e) => {
if (e.data.type === 'updateFavicon') {
document.querySelector('link[rel~="icon"]').href = e.data.dataUrl;
}
};
worker.postMessage({ type: 'init', images: ['icon-a.png', 'icon-b.png'] });
// favicon-worker.js 裡面
const canvas = new OffscreenCanvas(16, 16);
const ctx = canvas.getContext('2d');
// ...畫一幀...
const blob = await canvas.convertToBlob();
const reader = new FileReader();
reader.onloadend = () => self.postMessage({ type: 'updateFavicon', dataUrl: reader.result });
reader.readAsDataURL(blob);
如果你的應用屬於「使用者經常掛在後台」這種 —— 聊天客戶端、建置看板、監控工具 —— 那這一步值得做。否則主執行緒版本寫起來簡單,效果也夠用。
幾個值得記住的細節
用 16×16 或 32×32,別更大。 反正最終就只顯示這麼大,更大的 canvas 只會讓 data URL 更長、每幀 CPU 負擔更高。32×32 配上銳利像素是甜蜜點。
匯出成 PNG,別用 ICO。 canvas.toDataURL('image/png') 是唯一能穩定運作的格式。別想著自己手寫 ICO 編碼。
停下時記得還原原 favicon。 動畫啟動前先把 link.href 存起來,等操作結束或者 beforeunload 時把它還原。一直掛著半壞的動畫看起來很 bug。
別讓動畫無止盡地轉。 即使是很輕微的脈動,在手機上一樣會耗電。載入結束、使用者讀完通知、分頁失焦時就停下來。
簡單情境直接放靜態圖。 只想顯示「1 則未讀訊息」,把 favicon 換成靜態紅點版本就好,不必動畫。大多數人在動態 favicon 上做的事情其實都用力過頭了。
什麼時候該用它
動態 favicon 適合的場景:
- 動畫反映的是使用者真正在意的狀態(上傳、處理、新訊息)
- 頁面是那種容易被掛在後台的類型
- 靜態角標或只改標題列不足以傳達同樣的資訊
不適合的場景:
- 純粹是裝飾
- 分頁一打開就一直在轉
- 主要受眾跑在 Safari 或行動裝置,那邊幾乎沒法用
把上面這個 demo 抓過去,把顏色和形狀換成你的品牌風,就可以上線。完整原始碼就在這一頁裡,View Source 直接複製即可。
參考資料
- Aymkdn/animated-favicon(GitHub) —— 以 Web Worker 為基礎的動態 favicon 函式庫,分頁切到後台仍會繼續播放
- The Making of an Animated Favicon —— CSS-Tricks —— Chris Coyier 拆解 canvas 轉 favicon 的做法
- How to animate a favicon? —— Stack Overflow —— 經典討論串,多種方案與相容性註解
- OffscreenCanvas —— MDN —— 讓 Web Worker 動態 favicon 模式得以成立的關鍵 API
使用 favicon.im 快速檢查您的 favicon 是否配置正確。我們的免費工具確保您網站的 favicon 在所有瀏覽器和裝置上正確顯示。
免費公共服務
Favicon.im 是一個完全免費的公共服務,受到全球開發者的信賴。