Files
yellowjacket/e2e/specs/library-lazy.spec.ts
T
yonlu 620151aa41 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
2026-10-06 01:43:00 -04:00

59 lines
2.1 KiB
TypeScript

/**
* #280: launching the app must not load the track list.
*
* Every collection used to be 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 — for someone
* looking at Home, which draws none of it. Measured on 50 000 tracks:
* 543 MB of backend RSS at rest before, 296 MB after.
*
* The negative assertion is the point, and it needs its complement: "no
* `GetTrackTable`" also holds on a build that fetches nothing at all,
* so the same spec opens Tracks and watches it arrive.
*/
import { test, expect, bindingCalls, navigateTo } from '../support/fixtures.js';
const TRACKS = 'library.Library.GetTrackTable';
/** The seed's default page, which is what a launch lands on. */
const LANDING_VIEW = 'home';
test('launching on Home does not load the track list', async ({ app }) => {
// Past the store's idle warm-up: "nothing was fetched" has to mean
// nothing, not "nothing has happened yet".
await app.waitForTimeout(3_500);
const calls = await bindingCalls(app);
expect(calls, 'the warm-up for the small collections ran').toContain(
'library.Library.GetAlbums',
);
expect(calls, `the track list was fetched at launch (${LANDING_VIEW})`).not.toContain(
TRACKS,
);
});
test('opening Tracks loads it, and only then', async ({ app }) => {
await app.waitForTimeout(1_000);
expect(await bindingCalls(app)).not.toContain(TRACKS);
await navigateTo(app, 'tracks');
await expect(app.getByTestId('track-row').first()).toBeVisible();
expect(await bindingCalls(app)).toContain(TRACKS);
});
test('a hover on the nav item starts the load before the click', async ({ app }) => {
await app.waitForTimeout(1_000);
const nav = app.getByTestId('nav-tracks');
// The pointer entering the item is the whole mechanism; nothing is
// clicked, so the data must arrive because of the hover alone.
await nav.hover();
await expect
.poll(async () => (await bindingCalls(app)).includes(TRACKS))
.toBe(true);
});