diff --git a/extension/app.js b/extension/app.js
index b2ecb78c..4a39c743 100644
--- a/extension/app.js
+++ b/extension/app.js
@@ -16,6 +16,105 @@
'use strict';
+/* ----------------------------------------------------------------
+ FAVICON 缓存管理
+
+ 使用 chrome.storage.local 缓存 favicon,减少向 Google 发送请求。
+ 缓存有效期:7 天
+ ---------------------------------------------------------------- */
+
+const FAVICON_CACHE_KEY = 'faviconCache';
+const FAVICON_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 天
+
+// 内存缓存(当前会话快速访问)
+let faviconMemoryCache = {};
+
+/**
+ * getFaviconUrl(domain)
+ *
+ * 获取 favicon URL,优先使用缓存。
+ * 返回 base64 数据 URL 或 Google API URL。
+ */
+async function getFaviconUrl(domain) {
+ if (!domain) return null;
+
+ // 1. 检查内存缓存
+ if (faviconMemoryCache[domain]) {
+ return faviconMemoryCache[domain];
+ }
+
+ // 2. 检查持久缓存
+ try {
+ const { [FAVICON_CACHE_KEY]: cache = {} } = await chrome.storage.local.get(FAVICON_CACHE_KEY);
+ const cached = cache[domain];
+
+ if (cached && Date.now() - cached.timestamp < FAVICON_CACHE_TTL) {
+ faviconMemoryCache[domain] = cached.data;
+ return cached.data;
+ }
+ } catch {}
+
+ // 3. 无缓存,返回 Google API URL(首次加载后会通过 cacheFavicon 缓存)
+ return `https://www.google.com/s2/favicons?domain=${domain}&sz=16`;
+}
+
+/**
+ * cacheFavicon(domain, imgElement)
+ *
+ * 将已加载的 favicon 缓存到本地。
+ * 在 img.onload 中调用。
+ */
+async function cacheFavicon(domain, imgElement) {
+ if (!domain || !imgElement) return;
+
+ try {
+ // 将图片转换为 base64
+ const canvas = document.createElement('canvas');
+ canvas.width = 16;
+ canvas.height = 16;
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(imgElement, 0, 0, 16, 16);
+ const dataUrl = canvas.toDataURL('image/png');
+
+ // 更新内存缓存
+ faviconMemoryCache[domain] = dataUrl;
+
+ // 更新持久缓存
+ const { [FAVICON_CACHE_KEY]: cache = {} } = await chrome.storage.local.get(FAVICON_CACHE_KEY);
+ cache[domain] = {
+ data: dataUrl,
+ timestamp: Date.now()
+ };
+ await chrome.storage.local.set({ [FAVICON_CACHE_KEY]: cache });
+ } catch {}
+}
+
+/**
+ * cleanExpiredFaviconCache()
+ *
+ * 清理过期的 favicon 缓存。
+ * 在扩展启动时调用一次。
+ */
+async function cleanExpiredFaviconCache() {
+ try {
+ const { [FAVICON_CACHE_KEY]: cache = {} } = await chrome.storage.local.get(FAVICON_CACHE_KEY);
+ const now = Date.now();
+ let cleaned = false;
+
+ for (const domain of Object.keys(cache)) {
+ if (now - cache[domain].timestamp > FAVICON_CACHE_TTL) {
+ delete cache[domain];
+ cleaned = true;
+ }
+ }
+
+ if (cleaned) {
+ await chrome.storage.local.set({ [FAVICON_CACHE_KEY]: cache });
+ }
+ } catch {}
+}
+
+
/* ----------------------------------------------------------------
CHROME TABS — Direct API Access
@@ -757,6 +856,37 @@ function checkTabOutDupes() {
OVERFLOW CHIPS ("+N more" expand button in domain cards)
---------------------------------------------------------------- */
+/**
+ * buildFaviconImg(domain)
+ *
+ * 生成 favicon img 标签,支持缓存。
+ * 使用 data-domain 属性来识别域名,在 onload 时缓存。
+ */
+function buildFaviconImg(domain) {
+ if (!domain) return '';
+ return ``;
+}
+
+/**
+ * getFaviconFromMemory(domain)
+ *
+ * 从内存/本地存储快速获取缓存的 favicon。
+ * 返回 base64 数据 URL 或 null。
+ */
+function getFaviconFromMemory(domain) {
+ if (!domain) return null;
+ try {
+ return localStorage.getItem('favicon_mem_' + domain);
+ } catch {
+ return null;
+ }
+}
+
function buildOverflowChips(hiddenTabs, urlCounts = {}) {
const hiddenChips = hiddenTabs.map(tab => {
const label = cleanTitle(smartTitle(stripTitleNoise(tab.title || ''), tab.url), '');
@@ -767,9 +897,13 @@ function buildOverflowChips(hiddenTabs, urlCounts = {}) {
const safeTitle = label.replace(/"/g, '"');
let domain = '';
try { domain = new URL(tab.url).hostname; } catch {}
- const faviconUrl = domain ? `https://www.google.com/s2/favicons?domain=${domain}&sz=16` : '';
+ // 优先使用缓存的 favicon
+ const cachedFavicon = getFaviconFromMemory(domain);
+ const faviconImg = cachedFavicon
+ ? `
`
+ : buildFaviconImg(domain);
return `