perf(library): load a collection when a view needs it, not at startup

All four collections were fetched at DOMContentLoaded and refetched on
every invalidation, whichever view was showing. On a 26 138-track
library the track list was 20.5 MB of that, and encoding it cost the
backend ~170 MB of transient allocation — paid by someone looking at
Home, which draws none of it.

- The store warms only albums, artists and genres, on idle after first
  paint: 1.6 MB together, and what made those views instant.
- A nav item prefetches on hover and on keyboard focus, which is the
  ~100 ms before the click that a cold open would otherwise wait.
- An invalidation refetches what something had loaded, and nothing
  else — a scan no longer loads the track list of a library whose
  Tracks view nobody has opened.
- `index.html`'s first-paint `<track-list>` is `view-hidden`, and
  `index.ts` no longer activates it: it is markup, not a decision about
  which view the launch lands on, and activating it was what fetched
  the whole list for a landing on Home. The track list itself loads on
  view activation, which the shell drives.

Measured on 50 000 tracks: backend RSS at rest 543 → 296 MB, peak 571 →
296 MB, Go heap held 361 → 125 MB, JS heap 31.8 → 18 MB, binding bytes
at rest 35.9 → 12.1 MB, heap after a browse 36.5 → 22.7 MB. Tracks
first open 26 ms; slowest view open 57 ms.

Closes #280
This commit is contained in:
yonlu committed 2026-10-06 01:43:00 -04:00
1 parent 3d9828b847
commit 620151aa41
11 files changed
+538 -73

No files matched your search

+10 -1
View File
@@ -11,7 +11,8 @@ import '@components/library-filter/library-filter';
import '@components/library-status-indicator/library-status-indicator';
import { Events } from '../../src/events';
import { activeViewStore } from '@store/active-view-store';
import { emit, stub, flush, calls, lastArgs } from '@test/support/harness';
import { libraryStore } from '@store/library-store';
import { emit, stub, flush, calls, lastArgs, resetHarness } from '@test/support/harness';
import {
fixture,
shadow,
@@ -181,6 +182,14 @@ describe('<library-filter>', () => {
it('selects a library by id, and the merged view by empty string', async () => {
const el = await fixture('library-filter');
// The list has to be in use for the filter change to refetch it
// (#280): a collection nothing has loaded is not loaded to correct
// it, so this is the state the app is in when the Tracks view is up.
stub('library.Library.GetTrackTable', { strings: [''] });
await libraryStore.getTracks();
resetHarness();
stub('library.Library.GetTrackTable', { strings: [''] });
await flush();
await el.updateComplete;