fix(14-perf): fix scroll jumping and input latency

Root causes addressed:
- track-list had no _itemSize hint for flow layout — virtualizer
  defaulted to 100px, measured actual ~33px rows, then called
  _correctScrollError/scrollTo on every scroll causing visible jumps
- will-change:transform on virtualizer elements caused nested GPU
  layers (virtualizer positions children with transforms internally)
  adding compositor overhead instead of helping
- content-visibility:auto on album cards conflicted with virtualizer's
  own DOM recycling, causing redundant layout recalculation
- track-list visibilityChanged handler wrote to store synchronously
  on every event (per-item during scroll) without any throttling
- IIFE closure in renderTrackRow created a new function per row per render

Fixes applied:
- Add _itemSize:{height:33} + fixed height:33px on .track-row (matches
  queue-panel pattern that already worked smoothly)
- Add overflow-anchor:none on track-list virtualizer
- Remove will-change:transform from all 6 scroll containers
- Remove content-visibility:auto from album cards
- RAF-throttle visibilityChanged scroll position saves
- Replace IIFE with direct cols.map() in template
This commit is contained in:
2026-03-14 14:46:38 -04:00
parent 4b7d35d7ec
commit 3b2e189e7d
9 changed files with 45 additions and 30 deletions
@@ -231,7 +231,6 @@ export class ArtistsView
overflow-y: auto;
overflow-x: hidden;
contain: paint;
will-change: transform;
}
lit-virtualizer {
@@ -131,7 +131,6 @@ const gridStyles = css`
position: relative;
overflow-y: auto;
contain: paint;
will-change: transform;
}
/* ========================================
@@ -149,8 +148,6 @@ const gridStyles = css`
transform 0.15s ease;
box-sizing: border-box;
width: var(--card-width, 176px);
content-visibility: auto;
contain-intrinsic-size: auto var(--card-width, 176px) auto calc(var(--card-width, 176px) + 40px);
}
.album-card:hover {
@@ -235,7 +235,6 @@ export class GenresView
overflow-y: auto;
overflow-x: hidden;
contain: paint;
will-change: transform;
}
lit-virtualizer {
@@ -308,7 +308,6 @@ export class PlaylistView extends LitElement {
display: flex;
flex-direction: column;
contain: paint;
will-change: transform;
}
.playlist-item {
@@ -302,7 +302,6 @@ export class QueuePanel
flex: 1;
overflow-y: auto;
contain: paint;
will-change: transform;
overflow-anchor: none;
}
@@ -195,8 +195,15 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
private resizeObserver: ResizeObserver | null =
null;
private flowLayout = flow();
// _itemSize matches the fixed .track-row height (33px) so lit-virtualizer
// doesn't need to measure items. Without this hint, the default 100px
// estimate causes constant scroll error correction (scrollTo() calls)
// that produce visible jumping/skipping during scroll.
private flowLayout = flow({
_itemSize: { width: 100, height: 33 },
} as Parameters<typeof flow>[0]);
private hasRestoredScroll = false;
private scrollSaveRAFId: number | null = null;
// =================================================================
// Filtered / sorted tracks (memoised)
@@ -933,7 +940,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
overflow-y: auto;
user-select: none;
contain: paint;
will-change: transform;
overflow-anchor: none;
}
.track-row {
@@ -947,6 +954,8 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
cursor: default;
user-select: none;
overflow: hidden;
height: 33px;
box-sizing: border-box;
}
.track-row > * {
@@ -1227,7 +1236,16 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
}
}
this.libraryCtrl.setScrollPosition('tracks', first);
// RAF-throttle scroll position saves — at most once per frame.
// Without this, every visibilityChanged (fired per-item during
// scroll) writes to the store synchronously, adding main-thread
// work during the scroll hot path.
if (this.scrollSaveRAFId === null) {
this.scrollSaveRAFId = requestAnimationFrame(() => {
this.scrollSaveRAFId = null;
this.libraryCtrl.setScrollPosition('tracks', first);
});
}
};
// =================================================================
@@ -1640,6 +1658,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
// No inline closures — all events delegated via data-index
// on the virtualizer element (see firstUpdated).
const term = this.searchCtrl.term;
return html`
<div
class=${classMap({
@@ -1661,26 +1680,23 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
variant=${favVariant}
></wa-icon>
</div>
${(() => {
const term = this.searchCtrl.term;
return cols.map((col) => {
const val = col.accessor(track);
const centered = val === '\u2014';
const display = term
? highlightText(val, term)
: val;
${cols.map((col) => {
const val = col.accessor(track);
const centered = val === '\u2014';
const display = term
? highlightText(val, term)
: val;
return html`
<div class=${classMap({
cell: true,
'cell-center': centered,
'cell-right': !centered && col.align === 'right',
})}>
${display}
</div>
`;
});
})()}
return html`
<div class=${classMap({
cell: true,
'cell-center': centered,
'cell-right': !centered && col.align === 'right',
})}>
${display}
</div>
`;
})}
</div>
`;
};
+2
View File
@@ -59,3 +59,5 @@ export function SetContext(arg1:context.Context):Promise<void>;
export function SetRemovalHooks(arg1:library.RemovalHooks):Promise<void>;
export function SetRescanHooks(arg1:library.RescanHooks):Promise<void>;
export function SoftScanAllLibraries():Promise<void>;
+4
View File
@@ -113,3 +113,7 @@ export function SetRemovalHooks(arg1) {
export function SetRescanHooks(arg1) {
return window['go']['library']['Library']['SetRescanHooks'](arg1);
}
export function SoftScanAllLibraries() {
return window['go']['library']['Library']['SoftScanAllLibraries']();
}
+1 -1
View File
@@ -137,7 +137,7 @@ export namespace library {
export class ScanWarning {
filePath: string;
phase: string;
err: any;
err: string;
static createFrom(source: any = {}) {
return new ScanWarning(source);