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:
1 parent
3d9828b847
commit
620151aa41
11 files changed
+538
-73
No files matched your search
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* #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);
|
||||
});
|
||||
Reference in new issue
Block a user