Merge pull request 'perf(web): cache macro images in local PWA (1.1.2)' (#12) from perf/local-image-cache into main
Build Windows / preflight (push) Successful in 1s
Build Windows / build-windows (push) Successful in 5m15s

This commit was merged in pull request #12.
This commit is contained in:
2026-07-18 06:29:20 +00:00
4 changed files with 32 additions and 19 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Configuration and constants for MacroPad Server
VERSION = "1.1.1"
VERSION = "1.1.2"
DEFAULT_PORT = 40000
SETTINGS_FILE = "settings.json"
+1 -1
View File
@@ -1 +1 @@
1.1.1
1.1.2
+25 -16
View File
@@ -34,8 +34,10 @@ class MacroPadApp {
// Guards against out-of-order macro fetches
this._macroReqId = 0;
// Blob object URLs created for macro images (revoked on re-render)
this._objectUrls = [];
// Cache of image URL -> Promise<objectURL>. Macro images are
// content-addressed by uuid filename, so they are immutable; caching
// avoids re-fetching every image on each re-render.
this._imageCache = new Map();
// Set once a local-mode auth failure has been surfaced, to avoid
// reconnect loops and repeated toasts.
@@ -451,16 +453,12 @@ class MacroPadApp {
return headers;
}
// Fetch a macro image with the auth header and display it as a blob
// object URL, so no credential is ever placed on an <img> src.
// Fetch a macro image with the auth header and display it as a blob object
// URL, so no credential is ever placed on an <img> src. The fetched blob is
// cached (images are immutable per URL) and reused across re-renders.
async loadMacroImage(url, img, placeholder) {
try {
const res = await fetch(url, { headers: this.getAuthHeaders() });
if (!res.ok) throw new Error('image request failed');
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
// Track for best-effort revocation on the next render.
this._objectUrls.push(objectUrl);
const objectUrl = await this._getImageUrl(url);
img.src = objectUrl;
} catch (e) {
// Fall back to the placeholder if the image can't be loaded.
@@ -469,6 +467,23 @@ class MacroPadApp {
}
}
// Returns a cached Promise<objectURL> for an image URL, fetching once and
// retaining it for the page's lifetime (immutable content, nothing to
// invalidate); de-duplicates concurrent requests too.
_getImageUrl(url) {
let entry = this._imageCache.get(url);
if (entry) return entry;
entry = (async () => {
const res = await fetch(url, { headers: this.getAuthHeaders() });
if (!res.ok) throw new Error('image request failed');
const blob = await res.blob();
return URL.createObjectURL(blob);
})();
entry.catch(() => this._imageCache.delete(url));
this._imageCache.set(url, entry);
return entry;
}
// Rendering (safe DOM construction only - no user value reaches innerHTML)
renderTabs() {
const container = document.getElementById('tabs-container');
@@ -492,12 +507,6 @@ class MacroPadApp {
const container = document.getElementById('macro-grid');
if (!container) return;
// Best-effort: revoke object URLs from the previous render to avoid leaks.
if (this._objectUrls && this._objectUrls.length) {
this._objectUrls.forEach((u) => URL.revokeObjectURL(u));
}
this._objectUrls = [];
container.textContent = '';
const macroEntries = Object.entries(this.macros);
+5 -1
View File
@@ -338,7 +338,11 @@ class WebServer:
# Only serve files that resolve to inside the macro_images directory
if (os.path.commonpath([requested, images_real]) == images_real
and os.path.isfile(requested)):
return FileResponse(requested)
# Macro images are content-addressed (uuid filenames) and thus
# immutable, so let the browser cache them aggressively.
return FileResponse(requested, headers={
"Cache-Control": "private, max-age=31536000, immutable"
})
raise HTTPException(status_code=404, detail="Image not found")
@app.websocket("/ws")