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
@@ -1553,9 +1553,13 @@ export class QueuePanel
|
||||
indices: number[],
|
||||
) {
|
||||
const queueTracks = this.queue.tracks;
|
||||
const filePaths = indices
|
||||
.map((i) => queueTracks[i]?.filePath)
|
||||
.filter((fp): fp is string => fp != null);
|
||||
const filePaths: string[] = [];
|
||||
|
||||
for (const i of indices) {
|
||||
const path = queueTracks[i]?.filePath;
|
||||
|
||||
if (path != null) filePaths.push(path);
|
||||
}
|
||||
|
||||
await showBatchTrackDetailsForPaths(
|
||||
() => this.trackDetailsDialog,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { designTokens } from '../../styles/tokens.css';
|
||||
import type { DragActiveDetail } from '@utils/drag-controller';
|
||||
import { ActiveViewController } from '@store/controllers/active-view-controller';
|
||||
import { ViewVisibilityController } from '@store/controllers/view-visibility-controller';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { VIEW_META } from '../../services/view-meta';
|
||||
import type { View } from '../../services/view-meta';
|
||||
|
||||
@@ -352,6 +353,10 @@ export class AppSidebar extends LitElement {
|
||||
: 'false'}
|
||||
@click=${() =>
|
||||
this.navigate(item.id)}
|
||||
@mouseenter=${() =>
|
||||
this.prefetch(item.id)}
|
||||
@focus=${() =>
|
||||
this.prefetch(item.id)}
|
||||
@dragover=${(e: DragEvent) =>
|
||||
this.onNavDragOver(
|
||||
e,
|
||||
@@ -503,6 +508,19 @@ export class AppSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start loading what this view draws, on hover or keyboard focus.
|
||||
*
|
||||
* The ~100 ms before the click is the whole point: #280 stopped
|
||||
* fetching every collection at startup, and this is what keeps the
|
||||
* view that *is* opened from paying the whole payload after the
|
||||
* click. Nothing is awaited and nothing is reported here — the view
|
||||
* itself reports a failure, and this is the same request.
|
||||
*/
|
||||
private prefetch(view: View) {
|
||||
libraryStore.prefetch(view);
|
||||
}
|
||||
|
||||
private navigate(view: View) {
|
||||
// No optimistic highlight: the shell answers, and it answers
|
||||
// synchronously in `handleNavigate` before it awaits anything.
|
||||
|
||||
@@ -1336,11 +1336,6 @@ export class TrackList
|
||||
super.connectedCallback();
|
||||
this.restoreSortPreferences();
|
||||
|
||||
if (this.externalTracks) {
|
||||
this.tracks = this.externalTracks;
|
||||
} else {
|
||||
this.loadTracks();
|
||||
}
|
||||
this.resizeObserver = new ResizeObserver(
|
||||
() => {
|
||||
this.onHostResize();
|
||||
@@ -1390,10 +1385,33 @@ export class TrackList
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list when this becomes the view on screen (#280).
|
||||
*
|
||||
* Not on connection: `index.html` renders a `<track-list>` as the
|
||||
* main panel's first-paint content, so a connection-time fetch was
|
||||
* the whole library loaded at launch for a landing on Home — 12 MB
|
||||
* at 50 000 tracks, and the backend's peak RSS with it. The shell
|
||||
* activates this element only when a navigation lands on Tracks.
|
||||
*
|
||||
* Called on *every* activation, not just the first: the store may
|
||||
* have refetched while this view was off screen, and `getTracks()`
|
||||
* answers from its cache when nothing changed.
|
||||
*/
|
||||
protected override onViewActivate(): void {
|
||||
if (this.externalTracks) {
|
||||
this.tracks = this.externalTracks;
|
||||
} else {
|
||||
void this.loadTracks();
|
||||
}
|
||||
|
||||
this.attachListListeners();
|
||||
}
|
||||
|
||||
/** Document-level listeners belong to the *visible* list. A cached
|
||||
* list is never disconnected, so this is the only place they can be
|
||||
* taken down again. */
|
||||
protected override onViewActivate(): void {
|
||||
private attachListListeners(): void {
|
||||
this.listenWhileActive(document, 'click', this.clearSelectionHandler);
|
||||
this.listenWhileActive(
|
||||
document,
|
||||
@@ -1560,9 +1578,10 @@ export class TrackList
|
||||
this.selection.clear();
|
||||
}
|
||||
|
||||
// Re-fetch when the store delivers fresh
|
||||
// data after eager refetch on invalidation.
|
||||
if (!this.externalTracks) {
|
||||
// Re-fetch when the store delivers fresh data after an
|
||||
// invalidation. Off screen the list does nothing with it, and
|
||||
// the next activation re-reads the store (#280).
|
||||
if (!this.externalTracks && this.viewActive) {
|
||||
const cached =
|
||||
this.libraryCtrl.cachedTracks;
|
||||
|
||||
|
||||
Reference in new issue
Block a user