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:
+49
-25
@@ -1123,11 +1123,58 @@ 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 _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;
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 32;
|
||||||
|
canvas.height = 32;
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.drawImage(_faviconImage, 0, 0, 32, 32);
|
||||||
|
|
||||||
|
// Activity dot — brand amber (same as bell indicator)
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(24, 8, 7, 0, 2 * Math.PI); // top-right area
|
||||||
|
ctx.fillStyle = '#F1A640'; // var(--bell-color)
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = '#0D1117'; // var(--bg) — border for contrast
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
link.href = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the favicon with an activity dot if any session has unseen bells.
|
* 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.
|
* Uses a 32x32 canvas to draw the original favicon + a colored circle overlay.
|
||||||
* Restores the original favicon when there are no unseen bells.
|
* Restores the original favicon when there are no unseen bells.
|
||||||
|
* Delegates drawing to _drawFaviconBadge which manages the cached Image object.
|
||||||
*/
|
*/
|
||||||
function updateFaviconBadge() {
|
function updateFaviconBadge() {
|
||||||
var hasActivity = _currentSessions && _currentSessions.some(function (s) {
|
var hasActivity = _currentSessions && _currentSessions.some(function (s) {
|
||||||
@@ -1138,7 +1185,7 @@ function updateFaviconBadge() {
|
|||||||
document.querySelector('link[rel="icon"]');
|
document.querySelector('link[rel="icon"]');
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
// Cache the original favicon on first call
|
// Cache the original favicon href on first call
|
||||||
if (!_originalFavicon) _originalFavicon = link.href;
|
if (!_originalFavicon) _originalFavicon = link.href;
|
||||||
|
|
||||||
if (!hasActivity) {
|
if (!hasActivity) {
|
||||||
@@ -1147,30 +1194,7 @@ function updateFaviconBadge() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw favicon + activity dot on canvas
|
_drawFaviconBadge();
|
||||||
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);
|
|
||||||
|
|
||||||
// Activity dot — brand amber (same as bell indicator)
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(24, 8, 7, 0, 2 * Math.PI); // top-right area
|
|
||||||
ctx.fillStyle = '#F1A640'; // var(--bell-color)
|
|
||||||
ctx.fill();
|
|
||||||
ctx.strokeStyle = '#0D1117'; // var(--bg) — border for contrast
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
link.href = canvas.toDataURL('image/png');
|
|
||||||
};
|
|
||||||
img.src = _originalFavicon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 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