perf: batch thumbnail loading — one Wails call for all album art

Replace per-card GetThumbnail calls (10 round-trips) with a single
GetThumbnails batch call that fetches all visible album thumbnails
in one Wails bridge round-trip.

Backend GetThumbnails accepts []ThumbnailRequest and returns
map[mbid]→dataURL. Each request still checks library → disk cache
→ CAA in order, but the bridge overhead is 1 call instead of 10.

Frontend fires loadThumbnails() once after search results arrive.
Album cards render immediately with CAA URL fallback, then re-render
once the batch resolves with cached/local data URLs.
This commit is contained in:
2026-03-25 19:37:46 -04:00
parent 0761cff408
commit 0e376abfbb
4 changed files with 96 additions and 18 deletions
@@ -1,7 +1,8 @@
import { LitElement, html, css, nothing } from 'lit';
import { customElement, state, query as litQuery } from 'lit/decorators.js';
import { designTokens } from '../../styles/tokens.css';
import { Search, GetThumbnail } from '@go/explore/Service';
import { Search, GetThumbnails } from '@go/explore/Service';
import type { ThumbnailRequest } from '@go/explore/Service';
import type {
MBSearchResult,
MBArtist,
@@ -566,6 +567,8 @@ export class ExploreView extends LitElement {
}
this.results = result;
this.loadThumbnails();
const elapsed = (performance.now() - startTime).toFixed(0);
console.log(
`[explore] search completed: "${query}" in ${elapsed}ms — ` +
@@ -587,21 +590,67 @@ export class ExploreView extends LitElement {
/* ── Thumbnail Loading ── */
private loadThumbnail(mbid: string, albumName: string, artistName: string) {
// Don't re-fetch if already loading or cached.
if (this.thumbnailCache.has(mbid)) return;
private thumbnailBatchPending = false;
// Mark as loading to prevent duplicate requests.
this.thumbnailCache.set(mbid, '');
/**
* Load thumbnails for all visible album cards in one batched
* Wails call. Called after search results are set.
*/
private loadThumbnails() {
if (this.thumbnailBatchPending || !this.results?.releaseGroups?.length) {
return;
}
GetThumbnail(mbid, albumName || '', artistName || '').then((dataUrl) => {
if (dataUrl) {
this.thumbnailCache.set(mbid, dataUrl);
this.requestUpdate();
// Collect MBIDs that need fetching.
const requests: ThumbnailRequest[] = [];
for (const rg of this.results.releaseGroups) {
if (!this.thumbnailCache.has(rg.mbid)) {
requests.push({
mbid: rg.mbid,
albumName: rg.title || '',
artistName: rg.artistCredit || '',
});
}
}).catch(() => {
// Leave empty string in cache — fallback will show.
});
}
if (requests.length === 0) return;
this.thumbnailBatchPending = true;
GetThumbnails(requests)
.then((results) => {
let updated = false;
for (const [mbid, dataUrl] of Object.entries(results)) {
if (dataUrl) {
this.thumbnailCache.set(mbid, dataUrl);
updated = true;
}
}
// Mark MBIDs with no art so we don't re-request.
for (const req of requests) {
if (!this.thumbnailCache.has(req.mbid)) {
this.thumbnailCache.set(req.mbid, '');
}
}
if (updated) {
this.requestUpdate();
}
})
.catch(() => {
// Batch failed — mark all as attempted.
for (const req of requests) {
if (!this.thumbnailCache.has(req.mbid)) {
this.thumbnailCache.set(req.mbid, '');
}
}
})
.finally(() => {
this.thumbnailBatchPending = false;
});
}
/* ── Top Results ── */
@@ -892,11 +941,6 @@ export class ExploreView extends LitElement {
const artURL = cachedArt || CoverArtGroupURL(rg.mbid);
const year = extractYear(rg.firstReleaseDate);
// Kick off async thumbnail fetch if not cached.
if (!cachedArt) {
this.loadThumbnail(rg.mbid, rg.title, rg.artistCredit);
}
return html`
<div
class="album-card"
+8
View File
@@ -13,6 +13,14 @@ export function CoverArtURL(arg1:string):Promise<string>;
export function GetThumbnail(arg1:string, arg2:string, arg3:string):Promise<string>;
export interface ThumbnailRequest {
mbid: string;
albumName: string;
artistName: string;
}
export function GetThumbnails(arg1:ThumbnailRequest[]):Promise<Record<string, string>>;
export function LookupArtist(arg1:string):Promise<explore.MBArtist>;
export function LookupReleaseGroup(arg1:string):Promise<explore.MBReleaseGroup>;
+4
View File
@@ -22,6 +22,10 @@ export function GetThumbnail(arg1, arg2, arg3) {
return window['go']['explore']['Service']['GetThumbnail'](arg1, arg2, arg3);
}
export function GetThumbnails(arg1) {
return window['go']['explore']['Service']['GetThumbnails'](arg1);
}
export function LookupArtist(arg1) {
return window['go']['explore']['Service']['LookupArtist'](arg1);
}