fix(v2): batch gallery cover fetches into windows - #3765
Conversation
The v2 gallery hydrated each visible card with its own
GET /roms/{id}/simple request (one HTTP round trip and one DB lookup
per card). On a full grid that meant ~100 simultaneous requests, which
the browser's per-host connection cap serialized into slow waves,
taking 1-2 minutes to fill the grid on low-power devices. v1 loaded the
same library in 1-2 paginated requests.
Route both grid (GalleryShell viewport sync) and list (GameListRow
mount) hydration through the store's existing fetchWindowAt, which
aligns each visible position to its shared 72-item window and dedupes
against pending/loaded windows. A full viewport now resolves in a
handful of paginated requests instead of one per card. Drops the
now-unused per-card fetchRomAt/cancelFetchAt store actions.
Generated-By: PostHog Code
Task-Id: 6690877a-9c32-4c91-a78b-3d6fd3851fb7
Greptile SummaryThis PR batches v2 gallery cover hydration into shared windows. The main changes are:
Confidence Score: 4/5Visible cards can remain unloaded after a transient window failure.
frontend/src/v2/components/Gallery/GalleryShell.vue Important Files Changed
Reviews (1): Last reviewed commit: "fix(v2): batch gallery cover fetches int..." | Re-trigger Greptile |
syncFetches only runs on viewport changes, so a window whose fetch failed once would leave up to WINDOW_SIZE (72) visible cards stranded as skeletons on a static viewport until the user scrolled or the gallery context changed. Since only visible windows are ever fetched, a failed window was always visible when it failed. fetchWindowAt now schedules a bounded, backing-off refetch (up to 3 attempts, 2s/4s/6s) on failure so a transient blip self-heals. Retry timers and attempt counts are cleared on success and aborted alongside in-flight requests on any gallery-context switch, so we never retry against a stale context. Generated-By: PostHog Code Task-Id: 6690877a-9c32-4c91-a78b-3d6fd3851fb7
Switching from per-card to per-window fetching dropped the departure cancellation the per-card path had: scrolling through a large library started a request per 72-item window encountered and left every departed window downloading and applying in the background, the wasted network / backend / render work this change set out to avoid on low-power devices. Add store.syncVisibleWindows(positions): it starts the windows covering the visible positions (deduped in fetchWindowAt) and aborts any in-flight or retry-pending window that no longer covers one. The shell's debounced viewport sync now delegates to it. Also abort in-flight windows when the gallery unmounts (onBeforeRouteLeave doesn't reset the store), keeping the loaded cache so returning is still instant. Drops the shell's dead visiblePositions bookkeeping. Generated-By: PostHog Code Task-Id: 6690877a-9c32-4c91-a78b-3d6fd3851fb7
|
The batching direction gets a strong +1 from me. I independently measured the Three things before merge, first one matters most: 1. Window fetches carry the full sidecar payload and (when scoped) full
Suggestion: once 2. Cancel/re-want race strands skeletons. 3. Question: list mode's row-mount fetches vs the shell's window sync. Nit: after (Review AI-assisted, Claude Code; measurements from the #3768 investigation |
Once the bootstrap or window 0 has populated the filter panel, alpha strip, and id index, later window fetches only need their paged items. Send with_char_index=false and with_filter_values=false so the backend doesn't recompute those aggregations for every window. Generated-By: PostHog Code Task-Id: daf23950-b84a-4537-971c-3f85425c172c
Description
Explain the changes or enhancements you are proposing with this pull request.
The v2 gallery is dramatically slower than v1 to load cover thumbnails on low-power devices. On an Amlogic S905W2 TV box, a grid of ~108 covers takes 1-2 minutes to fill; the v1 UI loaded the same library in 1-2 seconds.
Root cause: v2 hydrated each visible card with its own
GET /roms/{id}/simplerequest, i.e. one HTTP round trip and one DB lookup per card. On a full grid that fires ~100 requests almost simultaneously. The browser's per-host connection limit (~6) serializes them into ~17 slow waves, and each wave pays full HTTP + JSON + a discrete backend query. v1 instead fetched a whole page (GET /roms?limit=72&offset=...) in 1-2 batched requests.Fix: route both layouts' hydration through the store's existing
fetchWindowAt, which aligns each visible position to its shared 72-item window and dedupes against pending/loaded windows:GalleryShell.syncFetchesnow callsfetchWindowAt(p)per visible position instead of per-cardfetchRomAt(p). Positions in the same window collapse to a single request (the store adds the offset topendingWindowssynchronously before its firstawait, so concurrent calls dedupe).GameListRowmount now callsfetchWindowAt(position).fetchRomAt/cancelFetchAtstore actions and their per-positionAbortControllerbookkeeping. Per-window fetches are still aborted en masse byinvalidateWindows/resetGalleryon context switch.A full viewport now resolves in a handful of paginated requests rather than one per card, matching v1's batched loading.
Note: two secondary GPU costs also compound this on weak GPUs but are out of scope here: the always-on full-viewport
filter: blur(28px)background layer, and the per-cardfilter: blur(16px)cover reveal animation. Happy to follow up on those separately.AI assistance disclosure: This change was authored with AI assistance (PostHog Code / Claude). The diagnosis, code changes, and comment updates were AI-generated and human-reviewed.
Checklist
Please check all that apply.
Screenshots (if applicable)
N/A (no visual change; loading behavior only).
Created with PostHog Code