perf(14-04): RAF-throttle scroll position saves and add overflow-anchor to queue panel

- 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
This commit is contained in:
2026-03-14 13:51:32 -04:00
parent ef18f59a9a
commit 6ca0b3c5a8
2 changed files with 117 additions and 22 deletions
@@ -3,9 +3,6 @@ import type { LitVirtualizer } from '@lit-labs/virtualizer';
import type { library } from '@go/models';
import type { LibraryController } from '@store/controllers/library-controller';
import {
SCROLL_DEBOUNCE_MS,
} from './cover-grid-types.js';
import type { GridEntry } from './cover-grid-types.js';
/**
@@ -45,10 +42,8 @@ export class ScrollManager {
private host: ScrollManagerHost;
private gc: GridConstants;
// Scroll position debounce.
private scrollDebounceTimer: ReturnType<
typeof setTimeout
> | null = null;
// RAF-throttled scroll position saving.
private scrollRAFId: number | null = null;
// Resize-aware scroll preservation.
private resizeObserver: ResizeObserver | null = null;
@@ -157,8 +152,8 @@ export class ScrollManager {
/** Clean up timers and observers. */
teardown(): void {
if (this.scrollDebounceTimer !== null) {
clearTimeout(this.scrollDebounceTimer);
if (this.scrollRAFId !== null) {
cancelAnimationFrame(this.scrollRAFId);
}
if (this.resizeDebounceTimer !== null) {
@@ -202,6 +197,12 @@ export class ScrollManager {
* Save scroll position from the first visible album.
* In split mode we use the before-entries; in single
* mode we use the full grid entries.
*
* Uses requestAnimationFrame throttling: saves at most
* once per frame (~16ms at 60fps). Unlike debouncing,
* this captures position continuously during scrolling
* (not just after it stops) and naturally aligns with
* the browser's paint cycle.
*/
onVisibilityChanged(
first: number,
@@ -209,21 +210,23 @@ export class ScrollManager {
): void {
if (this.isResizing) return;
if (this.scrollDebounceTimer !== null) {
clearTimeout(this.scrollDebounceTimer);
}
if (this.scrollRAFId !== null) return;
this.scrollDebounceTimer = setTimeout(() => {
const entries = getEntries();
const entry = entries[first];
this.scrollRAFId = requestAnimationFrame(
() => {
this.scrollRAFId = null;
if (entry) {
this.host.libraryCtrl.setScrollPosition(
'albums',
entry.albumIndex,
);
}
}, SCROLL_DEBOUNCE_MS);
const entries = getEntries();
const entry = entries[first];
if (entry) {
this.host.libraryCtrl.setScrollPosition(
'albums',
entry.albumIndex,
);
}
},
);
}
// ================================================================