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:
2026-08-12 01:19:37 -04:00
parent 4ae6e13391
commit 559e1ed077
7 changed files with 558 additions and 145 deletions
@@ -264,7 +264,19 @@ export class TopResultsRow extends LitElement {
: 'track';
return html`
<div class="card" @click=${() => this.handleClick(r)}>
<div
class="card"
role="button"
tabindex="0"
aria-label=${`${badgeLabel(r.entityType)}: ${r.name}`}
@click=${() => this.handleClick(r)}
@keydown=${(e: KeyboardEvent) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
e.preventDefault();
this.handleClick(r);
}}
>
<span
class="badge"
style="background: ${badgeColor(r.entityType)}"