- Replace wa-icon shadow DOM with inline SVG in queue-panel (xmark)
and album-dropdown (fav icons) to eliminate per-item shadow roots
- Memoize getBeforeEntries/getAfterEntries in cover-grid to prevent
.slice() creating new array refs that trigger virtualizer relayout
- Remove transition: scale and border-radius from album cards to
avoid per-frame repaints and anti-aliased path clipping
- Add queueMicrotask batching to player-store and favorites-store
notify() to coalesce rapid-fire updates into single renders
Targeted optimizations for the DMABuf-disabled rendering path where
every frame is software-composited:
- Replace infinite CSS scroll-text animation with transition-based
cycle that only repaints during active scroll, not during pauses
- Remove CSS mask-image on scrolling text (mask + animation was the
single most expensive continuous repaint)
- Replace wa-icon in track rows with inline SVG — eliminates 30-50
shadow DOM trees (each with SVG fetch/parse) during scroll
- Remove hover transitions on album cards, artist cards, genre cards,
fav icons, queue remove buttons — each transition was causing
per-frame software repaints
- Use visibility:hidden instead of opacity:0 for queue remove button
(binary switch vs per-frame alpha blend)
- Add decoding=async to now-playing cover art images (prevents
main-thread blocking during image decode on track change)
- Add contain:strict to fixed-height track rows (33px) and queue
items (49px) — browser skips size contribution calculations
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
- Replace inline arrow closures in renderTrackRow with event delegation via data-index
- Replace inline arrow closures in renderTrackItem with event delegation via data-index
- Add delegated click/dblclick/contextmenu/dragstart handlers on virtualizer elements
- Remove button click in queue panel also delegated via closest('.remove-button')
- Zero new function objects created per renderItem call during scroll
- Replace 100ms debounced scroll save in cover grid with requestAnimationFrame throttling
- Position now saves continuously during scrolling (~16ms) instead of only after stop
- Cancel pending RAF in teardown() to prevent leaks
- Add overflow-anchor: none CSS to queue panel lit-virtualizer
- Keep monkey-patch for lit-virtualizer _correctScrollError with expanded comment explaining why CSS alone is insufficient
- contain: layout style on :host of all 6 scroll-heavy components
- contain: paint + will-change: transform on all scroll containers for GPU compositing
- content-visibility: auto + contain-intrinsic-size on .album-card for off-screen skip
- cover-grid, track-list, queue-panel, artists-view, genres-view, playlist-view
lit-virtualizer's flow layout has a scroll error correction mechanism that
calls scrollTo() to fix sub-pixel estimation errors. On large lists (20k+),
even with fixed-height items, floating-point differences from
getBoundingClientRect() (e.g. 49.000003px vs 49px) accumulate across items
and trigger corrections that fight the native scrollbar drag gesture,
causing the thumb to desync from the mouse.
Detect scrollbar drag by checking if mousedown occurs in the scrollbar
gutter (clientX > element clientWidth), then monkey-patch the virtualizer's
_correctScrollError method to discard accumulated errors during drag
instead of calling scrollTo(). Items continue to render/recycle normally
since layout updates are not suppressed -- only the scroll position
corrections are skipped.
The previous fix (fixed CSS height on .track-item) was insufficient because
lit-virtualizer's flow layout defaults to estimating items at 100px tall.
With 20k items, the difference between 100px estimate and 49px actual creates
a ~1M px scroll height that collapses as items get measured, triggering
scroll error corrections (programmatic scrollTo calls) that fight the native
scrollbar during drag.
Setting _itemSize to { height: 49 } via the flow() config ensures the initial
scroll size estimate matches reality, eliminating the scroll error corrections
that caused the scrollbar thumb to desync from the mouse when dragging down.
- Add handleSelectAll bound handler calling selection.selectAll() in all three components
- Register/unregister event listeners in connectedCallback/disconnectedCallback
- Add height: 49px and overflow: hidden to .track-item CSS
- Add overflow: hidden to .track-details CSS
- Fixed height ensures lit-virtualizer _scrollSize is stable from first render
- Prevents scrollbar lag on large queues (20k+ tracks) caused by dynamic item size averaging
repeat() as child content of lit-virtualizer bypasses virtualization,
rendering ALL items instead of only visible ones. This caused 2+ minute
loading times and UI freezing with large libraries.
The correct approach: use lit-virtualizer's native .renderItem and
.keyFunction properties which integrate with its scroll-based viewport
management.
- cover-grid dynamic text tiers use --yj-text-xs/sm/md/lg tokens
- track-list sort toolbar, row text, and fav icon use design tokens
- queue-panel header and track text use type scale tokens
- track-details, track-info, artist-details, genre-details use tokens
- designTokens imported and prepended to static styles in all 8 files
- Hoist searchCtrl.term lookup outside per-column loop in renderTrackRow
- Search highlighting already short-circuits when search term is empty
- Import classMap in queue-panel.ts
- Replace array filter/join class construction with classMap in renderTrackItem
- Eliminates per-row array allocation in queue panel render hot path
album artist gets populated with artist as fallback (frontend also uses
this as display name fallback for albums). added scroll position
persistence when switching main views. also added frontend cache for
faster switching.