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 _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.
|
||||
* Uses a 32x32 canvas to draw the original favicon + a colored circle overlay.
|
||||
* Restores the original favicon when there are no unseen bells.
|
||||
* Draw the favicon activity badge onto the <link> element.
|
||||
* Owns the _faviconImage lifecycle: lazily creates it once (caching it in the module-level
|
||||
* 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() {
|
||||
var hasActivity = _currentSessions && _currentSessions.some(function (s) {
|
||||
return s.bell && s.bell.unseen_count > 0;
|
||||
});
|
||||
function _drawFaviconBadge() {
|
||||
// Lazy-init: create the Image object once and cache it — subsequent calls reuse it
|
||||
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"]') ||
|
||||
document.querySelector('link[rel="icon"]');
|
||||
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');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
var ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
var img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = function () {
|
||||
ctx.drawImage(img, 0, 0, 32, 32);
|
||||
ctx.drawImage(_faviconImage, 0, 0, 32, 32);
|
||||
|
||||
// Activity dot — brand amber (same as bell indicator)
|
||||
ctx.beginPath();
|
||||
@@ -1169,8 +1168,33 @@ function updateFaviconBadge() {
|
||||
ctx.stroke();
|
||||
|
||||
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 ────────────────────────────────────────────────────
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user