fix: cache favicon Image object — stop re-fetching favicon-32.png every 2s
updateFaviconBadge() was creating new Image() on every call (every 2s via poll cycle). Setting img.src triggers a network fetch of favicon-32.png each time, causing favicon-32.png to appear in network traffic alongside every heartbeat + sessions poll. Fix: extract _drawFaviconBadge() helper that owns the Image lifecycle. It lazily creates _faviconImage once (module-level var), caches it, and reuses on all subsequent calls. If the image isn't loaded yet it registers an onload callback to retry. updateFaviconBadge() simply calls _drawFaviconBadge() — no Image construction in the hot path.
This commit is contained in:
+47
-23
@@ -1123,41 +1123,40 @@ function updatePillBell() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var _originalFavicon = null; // cached original favicon href
|
var _originalFavicon = null; // cached original favicon href
|
||||||
|
var _faviconImage = null; // cached Image object for favicon badge compositing — avoids re-fetching every poll
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the favicon with an activity dot if any session has unseen bells.
|
* Draw the favicon activity badge onto the <link> element.
|
||||||
* Uses a 32x32 canvas to draw the original favicon + a colored circle overlay.
|
* Owns the _faviconImage lifecycle: lazily creates it once (caching it in the module-level
|
||||||
* Restores the original favicon when there are no unseen bells.
|
* variable) and reuses it on all subsequent calls. This avoids re-fetching favicon-32.png
|
||||||
|
* on every poll cycle (previously new Image() was created inside updateFaviconBadge every 2s).
|
||||||
|
* If the image is not yet loaded, registers an onload callback to retry automatically.
|
||||||
*/
|
*/
|
||||||
function updateFaviconBadge() {
|
function _drawFaviconBadge() {
|
||||||
var hasActivity = _currentSessions && _currentSessions.some(function (s) {
|
// Lazy-init: create the Image object once and cache it — subsequent calls reuse it
|
||||||
return s.bell && s.bell.unseen_count > 0;
|
if (!_faviconImage) {
|
||||||
});
|
_faviconImage = new Image();
|
||||||
|
_faviconImage.crossOrigin = 'anonymous';
|
||||||
|
_faviconImage.src = _originalFavicon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If image is not yet loaded, wait for it (onload will call us back)
|
||||||
|
if (!_faviconImage.complete || _faviconImage.naturalWidth === 0) {
|
||||||
|
_faviconImage.onload = function() { _drawFaviconBadge(); };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var link = document.querySelector('link[rel="icon"][sizes="32x32"]') ||
|
var link = document.querySelector('link[rel="icon"][sizes="32x32"]') ||
|
||||||
document.querySelector('link[rel="icon"]');
|
document.querySelector('link[rel="icon"]');
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
// Cache the original favicon on first call
|
|
||||||
if (!_originalFavicon) _originalFavicon = link.href;
|
|
||||||
|
|
||||||
if (!hasActivity) {
|
|
||||||
// Restore original favicon when no activity
|
|
||||||
if (link.href !== _originalFavicon) link.href = _originalFavicon;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw favicon + activity dot on canvas
|
|
||||||
var canvas = document.createElement('canvas');
|
var canvas = document.createElement('canvas');
|
||||||
canvas.width = 32;
|
canvas.width = 32;
|
||||||
canvas.height = 32;
|
canvas.height = 32;
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
var img = new Image();
|
ctx.drawImage(_faviconImage, 0, 0, 32, 32);
|
||||||
img.crossOrigin = 'anonymous';
|
|
||||||
img.onload = function () {
|
|
||||||
ctx.drawImage(img, 0, 0, 32, 32);
|
|
||||||
|
|
||||||
// Activity dot — brand amber (same as bell indicator)
|
// Activity dot — brand amber (same as bell indicator)
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -1169,8 +1168,33 @@ function updateFaviconBadge() {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
link.href = canvas.toDataURL('image/png');
|
||||||
};
|
}
|
||||||
img.src = _originalFavicon;
|
|
||||||
|
/**
|
||||||
|
* Update the favicon with an activity dot if any session has unseen bells.
|
||||||
|
* Uses a 32x32 canvas to draw the original favicon + a colored circle overlay.
|
||||||
|
* Restores the original favicon when there are no unseen bells.
|
||||||
|
* Delegates drawing to _drawFaviconBadge which manages the cached Image object.
|
||||||
|
*/
|
||||||
|
function updateFaviconBadge() {
|
||||||
|
var hasActivity = _currentSessions && _currentSessions.some(function (s) {
|
||||||
|
return s.bell && s.bell.unseen_count > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var link = document.querySelector('link[rel="icon"][sizes="32x32"]') ||
|
||||||
|
document.querySelector('link[rel="icon"]');
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
// Cache the original favicon href on first call
|
||||||
|
if (!_originalFavicon) _originalFavicon = link.href;
|
||||||
|
|
||||||
|
if (!hasActivity) {
|
||||||
|
// Restore original favicon when no activity
|
||||||
|
if (link.href !== _originalFavicon) link.href = _originalFavicon;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawFaviconBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Session open / close ────────────────────────────────────────────────────
|
// ─── Session open / close ────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -3388,6 +3388,16 @@ test('pollSessions calls updateFaviconBadge', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updateFaviconBadge caches Image object instead of fetching every call', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
assert.ok(source.includes('_faviconImage'), 'must cache favicon Image object');
|
||||||
|
assert.ok(source.includes('_drawFaviconBadge'), 'must use extracted draw helper');
|
||||||
|
// The old pattern: new Image() inside updateFaviconBadge should be gone
|
||||||
|
const fnStart = source.indexOf('function updateFaviconBadge');
|
||||||
|
const fnBody = source.substring(fnStart, fnStart + 1000);
|
||||||
|
assert.ok(!fnBody.includes('new Image()'), 'must NOT create new Image on every call');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// --- Delete session template (task: customizable delete command) ---
|
// --- Delete session template (task: customizable delete command) ---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user