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:
@@ -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
@@ -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>;
|
||||
|
||||
@@ -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']();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user