perf(frontend): make the lists and grids pay per row, not per library
A list pays per row, and only while scrolling — and none of this is visible to any test tier: nothing renders differently and nothing fails, the app is just slower. - The track list's Art column rendered `CoverArtPath`, the original artwork, into a 24 px box while `CoverArtSmall` sat unused on the same model, with no `loading="lazy"`. 26 of 26 image requests asked for the full-size tier; now 0. - `artists-view`'s avatar fallback linear-scanned every cached album per card per frame, lowercasing two strings per comparison, inside the virtualizer's renderItem — the common case, since a locally tagged library has no artist images at all. Measured at 5 000 albums and 24 visible cards: 1.46 ms/frame -> 0.01 ms/frame. - Five components resolved selected file paths back to tracks with `tracks.find(...)`; they share `utils/track-index.ts` now. "Select all -> Edit tags" at 50 000 tracks: 3 051-6 298 ms -> 68 ms. - "Play this artist", "play these albums" and the album drag cache resolve paths in one call instead of one per album. - The column-resize drag registers its document listeners on mousedown. Two things here are load-bearing and read as sloppiness. The per-render arrow functions in `artists-view` and `genres-view` are the *only* thing changing a property of their virtualizer on a host update, and therefore the only thing repainting the cards: hoisting them to stable fields takes a selection from 1 highlighted card to 0. And a row inside a virtualizer needs `width: 100%`, because the virtualizer positions its children absolutely and a grid row otherwise shrinks to fit its content and stops lining up with the header above it.
This commit is contained in:
@@ -11,8 +11,7 @@ import type {
|
||||
} from '@lit-labs/virtualizer';
|
||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||
import {
|
||||
GetTracksByGenre,
|
||||
GetTracksByGenreByLibrary,
|
||||
GetFilePathsByGenres,
|
||||
} from '@go/library/Library';
|
||||
import type { library } from '@go/models';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
@@ -24,6 +23,8 @@ import {
|
||||
} from '@utils/context-menu-controller.js';
|
||||
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||
import { RovingGridController } from '@utils/roving-grid';
|
||||
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
@@ -59,7 +60,7 @@ interface GenreEntry {
|
||||
|
||||
@customElement('genres-view')
|
||||
export class GenresView
|
||||
extends LitElement
|
||||
extends ViewLifecycleMixin(LitElement)
|
||||
implements ContextMenuHost
|
||||
{
|
||||
private libraryCtrl = new LibraryController(this);
|
||||
@@ -69,6 +70,18 @@ export class GenresView
|
||||
private wheelListenerAttached = false;
|
||||
private lastSearchTerm = '';
|
||||
|
||||
/** See artists-view: one tab stop for the grid, moved with the
|
||||
* arrow keys. */
|
||||
private roving = new RovingGridController(this, {
|
||||
cardSelector: '.genre-card',
|
||||
count: () => this.cachedGridEntries.length,
|
||||
scrollToIndex: (index) => {
|
||||
this.shadowRoot
|
||||
?.querySelector<LitVirtualizer>('lit-virtualizer')
|
||||
?.scrollToIndex(index, 'nearest');
|
||||
},
|
||||
});
|
||||
|
||||
/** Tracks the store's cached array reference to detect refreshes. */
|
||||
private lastGenresRef:
|
||||
| library.GenreWithCount[]
|
||||
@@ -400,9 +413,16 @@ export class GenresView
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.detachWheelListener();
|
||||
}
|
||||
|
||||
/** See artists-view: off-screen the grid cannot be scrolled, and
|
||||
* being cached it is never disconnected. */
|
||||
protected override onViewDeactivate(): void {
|
||||
this.detachWheelListener();
|
||||
|
||||
if (this.scrollDebounceTimer !== null) {
|
||||
clearTimeout(this.scrollDebounceTimer);
|
||||
this.scrollDebounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,24 +757,24 @@ export class GenresView
|
||||
const libId =
|
||||
this.libraryCtrl.selectedLibraryId;
|
||||
|
||||
const promises = Array.from(
|
||||
genreNames,
|
||||
(name) =>
|
||||
libId !== null
|
||||
? GetTracksByGenreByLibrary(
|
||||
name,
|
||||
libId,
|
||||
)
|
||||
: GetTracksByGenre(name),
|
||||
// perf.m2: one call per genre, each returning
|
||||
// whole track rows so the file path could be
|
||||
// read off them — 6 MB over the IPC for five
|
||||
// genres of a 50 000-track library.
|
||||
const names = Array.from(genreNames);
|
||||
const byGenre = await GetFilePathsByGenres(
|
||||
names,
|
||||
libId ?? 0,
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
for (const tracks of results) {
|
||||
for (const track of tracks ?? []) {
|
||||
if (!seen.has(track.FilePath)) {
|
||||
seen.add(track.FilePath);
|
||||
allPaths.push(track.FilePath);
|
||||
// Still de-duplicated here: a track with two of
|
||||
// the selected genres appears under both, and
|
||||
// the caller owns the order.
|
||||
for (const name of names) {
|
||||
for (const path of byGenre[name] ?? []) {
|
||||
if (!seen.has(path)) {
|
||||
seen.add(path);
|
||||
allPaths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -945,7 +965,9 @@ export class GenresView
|
||||
class="genre-card${isSelected
|
||||
? ' selected'
|
||||
: ''}"
|
||||
tabindex="0"
|
||||
data-index=${index}
|
||||
tabindex=${this.roving.tabIndexFor(index)}
|
||||
@focus=${() => this.roving.noteFocus(index)}
|
||||
role="button"
|
||||
aria-label="${genre.name}"
|
||||
aria-selected="${isSelected}"
|
||||
@@ -1190,6 +1212,7 @@ export class GenresView
|
||||
? 'visibility: hidden'
|
||||
: ''}
|
||||
@click=${this.onGridClick}
|
||||
@keydown=${this.roving.handleKeydown}
|
||||
>
|
||||
<lit-virtualizer
|
||||
.items=${entries}
|
||||
|
||||
Reference in New Issue
Block a user