From 3b2e189e7d0e6d00393d087565190fd307774257 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sat, 14 Mar 2026 14:46:38 -0400 Subject: [PATCH] fix(14-perf): fix scroll jumping and input latency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/artists-view/artists-view.ts | 1 - .../cover-grid/cover-grid-styles.ts | 3 - .../src/components/genres-view/genres-view.ts | 1 - .../components/playlist-view/playlist-view.ts | 1 - .../src/components/queue-panel/queue-panel.ts | 1 - .../src/components/track-list/track-list.ts | 60 ++++++++++++------- frontend/wailsjs/go/library/Library.d.ts | 2 + frontend/wailsjs/go/library/Library.js | 4 ++ frontend/wailsjs/go/models.ts | 2 +- 9 files changed, 45 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts index f6d71d9..c0eae57 100644 --- a/frontend/src/components/artists-view/artists-view.ts +++ b/frontend/src/components/artists-view/artists-view.ts @@ -231,7 +231,6 @@ export class ArtistsView overflow-y: auto; overflow-x: hidden; contain: paint; - will-change: transform; } lit-virtualizer { diff --git a/frontend/src/components/cover-grid/cover-grid-styles.ts b/frontend/src/components/cover-grid/cover-grid-styles.ts index c013970..fe1bb08 100644 --- a/frontend/src/components/cover-grid/cover-grid-styles.ts +++ b/frontend/src/components/cover-grid/cover-grid-styles.ts @@ -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 { diff --git a/frontend/src/components/genres-view/genres-view.ts b/frontend/src/components/genres-view/genres-view.ts index f3ccb4a..2ccb6eb 100644 --- a/frontend/src/components/genres-view/genres-view.ts +++ b/frontend/src/components/genres-view/genres-view.ts @@ -235,7 +235,6 @@ export class GenresView overflow-y: auto; overflow-x: hidden; contain: paint; - will-change: transform; } lit-virtualizer { diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts index e330b33..b6bf585 100644 --- a/frontend/src/components/playlist-view/playlist-view.ts +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -308,7 +308,6 @@ export class PlaylistView extends LitElement { display: flex; flex-direction: column; contain: paint; - will-change: transform; } .playlist-item { diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index 5926c94..6721c2b 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -302,7 +302,6 @@ export class QueuePanel flex: 1; overflow-y: auto; contain: paint; - will-change: transform; overflow-anchor: none; } diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index 7ee2394..7cb6185 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -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[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`
- ${(() => { - 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` -
- ${display} -
- `; - }); - })()} + return html` +
+ ${display} +
+ `; + })} `; }; diff --git a/frontend/wailsjs/go/library/Library.d.ts b/frontend/wailsjs/go/library/Library.d.ts index 44b7ea3..9e0486c 100755 --- a/frontend/wailsjs/go/library/Library.d.ts +++ b/frontend/wailsjs/go/library/Library.d.ts @@ -59,3 +59,5 @@ export function SetContext(arg1:context.Context):Promise; export function SetRemovalHooks(arg1:library.RemovalHooks):Promise; export function SetRescanHooks(arg1:library.RescanHooks):Promise; + +export function SoftScanAllLibraries():Promise; diff --git a/frontend/wailsjs/go/library/Library.js b/frontend/wailsjs/go/library/Library.js index 4678d3e..39ecfaa 100755 --- a/frontend/wailsjs/go/library/Library.js +++ b/frontend/wailsjs/go/library/Library.js @@ -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'](); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index b361e51..9af9d09 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -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);