Skip to content

perf(library): prefetch inactive tabs so switching feels instant#61

Closed
InstaZDLL wants to merge 1 commit into
mainfrom
perf/library-tab-switch-instant
Closed

perf(library): prefetch inactive tabs so switching feels instant#61
InstaZDLL wants to merge 1 commit into
mainfrom
perf/library-tab-switch-instant

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 18, 2026

Summary

User-reported pain: clicking Artistes / Albums / Morceaux for the first time leaves the user staring at a blank tab for 1–2 s while the backend runs the corresponding list_* query and the JSON crosses Tauri's IPC bridge. Subsequent visits to the same tab were already instant β€” each tab's state array persists across activeTab switches β€” but the cold-cache first click was painful.

Approach

Reshape the loading logic in LibraryView so the user only pays the fetch cost once per cache-invalidation, regardless of which tab they click first:

  1. tabCacheKeys memo β€” derives one stable key per tab from the inputs that legitimately invalidate its data (library set, sort, filterNoCover, cover-reload signal, edit signal).
  2. loadedKeysRef β€” records which key currently lives in each tab's state array. The foreground loader short-circuits when the active tab already matches its desired key (e.g. the user clicked back to a tab that just got prefetched).
  3. Background prefetch effect β€” waits 200 ms after the active tab settles, then serially fetches each non-active tab whose stored key is stale. Serial (not parallel) so a large list_tracks query doesn't fight the active tab for SQLite pool slots.
  4. fetchTab / applyTab helpers β€” centralise the per-tab switch so the two paths can't drift apart on the next refactor.

Net effect

  • The very first foreground tab loads exactly as before.
  • Every subsequent tab switch renders the cached data instantly.
  • A silent refresh runs in the background only when the cache key has actually invalidated (sort change, library change, edit, etc.).
  • Failures in the background path are swallowed and fall back to the foreground loader on user click β€” no regression on the worst case.

Test plan

  • bun run typecheck
  • bun run lint
  • Cold launch β†’ first tab (Songs/Tracks) loads normally β†’ click Albums within 200 ms+~query time β†’ instant render
  • Change sort on Albums β†’ background prefetch refreshes other tabs (verify by switching back to Songs and checking the data picks up the same editRefetch/sort changes)
  • Edit a track in Library β†’ all tabs visible data refreshes within ~200 ms after the foreground load

Summary by CodeRabbit

  • Refactor
    • OptimisΓ© le chargement des onglets de la bibliothΓ¨que avec des mΓ©canismes de mise en cache amΓ©liorΓ©s et une prΓ©-rΓ©cupΓ©ration des donnΓ©es en arriΓ¨re-plan, offrant une navigation par onglets plus fluide et rΓ©active.

Review Change Stack

Reported pain: clicking Artistes / Albums / Morceaux for the first
time leaves the user staring at a blank tab for 1-2 s while the
backend runs `list_artists` / `list_albums` / `list_tracks` and the
result crosses Tauri's IPC bridge. Subsequent visits were already
instant because each tab's state array persists across switches, so
the only gap was the cold cache on first click.

Reshape the loading logic:

1. A `tabCacheKeys` memo derives one stable key per tab from the
   inputs that legitimately invalidate its data (library set, sort,
   `filterNoCover`, cover-reload, edit signal). The same key drives
   foreground and background paths.

2. `loadedKeysRef` records which key currently lives in each tab's
   state array. The foreground loader now short-circuits when the
   active tab already matches its desired key (e.g. the user clicked
   back to a tab that just got prefetched).

3. A second effect waits 200 ms after the active tab settles, then
   serially fetches each non-active tab whose stored key is stale.
   Best-effort: a failure just defers the work to the foreground
   loader when the user clicks. Serial (not parallel) so a large
   `list_tracks` query doesn't fight the active tab for SQLite pool
   slots.

`fetchTab` / `applyTab` helpers centralise the per-tab branch so the
two paths can't drift apart on the next refactor.

Net effect: the first foreground tab still loads as before; every
subsequent tab change renders the cached data instantly while a
silent refresh runs in the background only when the cache key has
actually invalidated.
@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) type: perf Performance improvement labels May 18, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4740bc6f-e7a4-4ef3-bd03-a91861d4167b

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 77bad0a and 5061aa6.

πŸ“’ Files selected for processing (1)
  • src/components/views/LibraryView.tsx

πŸ“ Walkthrough

Walkthrough

LibraryView implémente un système de cache par onglet avec des clés de cache dépendant des libraries, tris et filtres. Le chargement foreground ne recharge l'onglet actif que si sa clé a changé. Une pré-récupération background charge les autres onglets après ~200 ms avec gestion d'erreur silencieuse.

Changes

Système de cache par onglet et préfetch background

Layer / File(s) Summary
Calcul des clΓ©s de cache et chargement foreground
src/components/views/LibraryView.tsx
Ajout de useMemo pour le calcul des clΓ©s de cache par onglet. Factorisation des fonctions fetchTab et applyTab. Le useEffect foreground charge l'onglet actif uniquement si sa clΓ© de cache n'a pas dΓ©jΓ  Γ©tΓ© appliquΓ©e (vΓ©rifiΓ©e via loadedKeysRef).
Pré-récupération en arrière-plan
src/components/views/LibraryView.tsx
Ajout d'un useEffect de prΓ©fetch dΓ©clenchΓ© aprΓ¨s ~200 ms : sΓ©lection des onglets inactifs Γ  cache invalidΓ©, requΓͺtes en sΓ©rie, mise Γ  jour des Γ©tats via applyTab, et stockage dans loadedKeysRef. Les erreurs sont ignorΓ©es (fallback au rechargement foreground au clic).

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

πŸš€ Cache keys calculΓ©es, onglets en mΓ©moire,
Foreground charge, background prΓ©pare,
DΓ©lai optimal, erreurs pardonnΓ©es,
LibraryView respire enfin ! ✨

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (4 passed)
Check name Status Explanation
Title check βœ… Passed Le titre suit le format Conventional Commits avec le scope Β« library Β» en kebab-case et dΓ©crit clairement l'objectif principal : optimiser le chargement des onglets inactifs pour accΓ©lΓ©rer les changements d'onglet.
Description check βœ… Passed La description couvre le problΓ¨me utilisateur, l'approche technique, les rΓ©sultats nets et inclut un plan de test. Tous les Γ©lΓ©ments clΓ©s du template sont prΓ©sents (summary, approach, test plan), bien que certains tests manuels ne soient pas cochΓ© comme complΓ©tΓ©s.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/library-tab-switch-instant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size: l 200-500 lines label May 18, 2026
@InstaZDLL
Copy link
Copy Markdown
Owner Author

Reverting β€” I misread the original complaint. Daisy said the tab itself opened instantly and only the artwork flickered for 1-2 s. My prefetcher kept SQLite busy in the background, so when the user clicked a new tab the foreground fetch queued behind a slow list_tracks/etc. and the perceived lag got worse. Closing without merging and tackling the actual issue (artwork load smoothness) in a separate PR.

@InstaZDLL InstaZDLL closed this May 18, 2026
@InstaZDLL InstaZDLL deleted the perf/library-tab-switch-instant branch May 18, 2026 21:24
InstaZDLL added a commit that referenced this pull request May 18, 2026
…rey (#62)

* fix(library): fade artwork over its gradient placeholder instead of grey

Reported pain: opening Artists or Albums showed each thumbnail
flashing through a grey square before the bytes arrived, for 1-2 s
across the whole grid. Tabs themselves opened instantly (state
arrays persist between switches) β€” the perceived lag was entirely
the artwork pop-in.

Replace the `bg-zinc-100` placeholder behind each <img> with the
same coloured gradient already used as the "no image" fallback
(emerald for Artwork, violet for artist avatars). The <img> mounts
with opacity-0 and fades to 100 on `onLoad`, so the placeholder
gradient stays put underneath until the bytes are actually decoded.

WebView-cached images that resolve synchronously skip onLoad
entirely; the `ref` callback catches `complete && naturalWidth > 0`
and pins the fade open immediately so we don't get stuck at zero.

The shared logic lives in a new `<FadeInImage>` so both `Artwork`
(album covers, track thumbnails) and the bespoke round artist
avatars in LibraryView render through the same primitive. Falls
back to React's documented "compare prev prop in render" reset
pattern to satisfy react-hooks/set-state-in-effect.

Closes the gap the reverted #61 perf branch tried β€” and missed β€”
to address.

* perf(library): virtualize Albums and Artists grids

Reported pain: switching to Albums or Artists with an 800-item
library mounted 800 React subtrees at once. Each tile carries an
<Artwork>/<FadeInImage> (state + useMemo) plus a popover-trigger
button, so the main thread stalled for ~1 s before the first paint
and then the IMG tags fanned out and filled in. The fade-in pass
from the previous PR couldn't disguise the layout cost.

Apply the TrackTable pattern to both grids: a row-level
`useVirtualizer`, with column count derived from the container's
width via ResizeObserver to match the original
`grid-cols-[repeat(auto-fill,minmax(180px,1fr))]` math. Each
virtual row renders `colCount` tiles; everything off-screen stays
out of the DOM.

Both grids consume `usePageScroll()` so the single page-driven
scrollbar is preserved (per the existing cross-cutting rule).

AlphabetIndex side-effect: `querySelector('[data-artist-index]') +
scrollIntoView` no longer works because the target artist may not
be mounted yet. ArtistList now exposes a `scrollToIndexRef`
callback the parent installs once on mount; the alphabet jump
delegates to `virtualizer.scrollToIndex(floor(idx / cols))`.

GenreList isn't touched β€” it tops out around 30-50 items in
practice and the fixed-height row card has none of the per-tile
state Artwork carries, so it'd be all cost and no benefit.

* fix(library): render artist avatars from the full-res source

The Artistes grid was still asking `resolveArtwork` for the 2x
(128 px) thumbnail variant β€” fine on a 64 px row icon, soft on the
180-220 px round tiles the virtualized grid actually paints on a
1080p / HiDPI screen. AlbumGrid had already switched to `size=full`
for the same reason; just bring artists in line.

Source artist images are the original Deezer 1000 px PNGs (or the
user's sidecar JPEGs in the same range), so decoding the full-res
copy at avatar size is cheap. No new fetch is introduced β€” the
`full` slot was already populated by `list_artists` since #60.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: frontend React/Vite frontend (src/) size: l 200-500 lines type: perf Performance improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant