perf(zoom): only register wheel listener while Ctrl/Meta is held

The zoom-on-Ctrl-wheel handler was registered on `window` for the
lifetime of the app with `{ passive: false, capture: true }`. That
flag combination forces the browser to wait for the handler to return
on EVERY wheel event before applying scroll — even though the handler
exits at line 1 when no modifier is held. The user reported a
characteristic input lag: scrollbar-drag was perfectly smooth, mouse-wheel
was choppy. His description ("como si la ventana principal lo cachara y
lo mandara al div interno y eso causara ese input lag") was literal —
the capture-phase non-passive listener intercepts the event at window
before routing to the scroll container.

Refactor: track Ctrl/Meta state via keydown/keyup and register the
non-passive wheel listener only while a modifier is actually held.
Default state = no wheel listener at all → browser native fast-path.
`window blur` releases the listener if the user alt-tabs with Ctrl held
(otherwise keyup never fires).

cc
This commit is contained in:
vicrodh committed 2026-05-11 22:38:23 -06:00
1 parent 67118b21e4
commit 29a6f386ce
1 file changed
+52 -3
+52 -3
View File
@@ -75,10 +75,59 @@ function handleZoomWheel(event: WheelEvent): void {
void applyZoomLevel(nextZoom); void applyZoomLevel(nextZoom);
} }
/**
* Zoom-on-Ctrl-wheel handling.
*
* Previously this registered a non-passive, capture-phase `wheel` listener
* on `window` for the lifetime of the app. The non-passive flag (required
* so `preventDefault` actually stops the WebView's native Ctrl+wheel zoom)
* also tells the browser "wait for my handler before applying scroll" —
* which kills the native fast-path for *every* wheel event in the app,
* not just zoom-modified ones. The user perceived this as choppy mouse-wheel
* scrolling vs perfectly-smooth scrollbar-drag scrolling.
*
* New approach: the wheel listener only exists while Ctrl/Meta is held.
* Keydown registers it, keyup (or window blur) unregisters it. Net:
* - 99% of normal scrolling: no wheel listener at all, browser fast-path
* - During zoom (Ctrl held): non-passive listener active, preventDefault
* still blocks native zoom and our `applyZoomLevel` runs
*/
const ZOOM_WHEEL_OPTIONS: AddEventListenerOptions = { passive: false, capture: true };
let zoomWheelActive = false;
function registerZoomWheel(): void {
if (zoomWheelActive) return;
zoomWheelActive = true;
window.addEventListener('wheel', handleZoomWheel, ZOOM_WHEEL_OPTIONS);
}
function unregisterZoomWheel(): void {
if (!zoomWheelActive) return;
zoomWheelActive = false;
window.removeEventListener('wheel', handleZoomWheel, ZOOM_WHEEL_OPTIONS);
}
function handleZoomKeyDown(e: KeyboardEvent): void {
if (e.ctrlKey || e.metaKey) registerZoomWheel();
}
function handleZoomKeyUp(e: KeyboardEvent): void {
// After any keyup, if neither modifier is held anymore, drop the listener.
if (!e.ctrlKey && !e.metaKey) unregisterZoomWheel();
}
export function setupZoomControls(): () => void { export function setupZoomControls(): () => void {
const options: AddEventListenerOptions = { passive: false, capture: true }; window.addEventListener('keydown', handleZoomKeyDown);
window.addEventListener('wheel', handleZoomWheel, options); window.addEventListener('keyup', handleZoomKeyUp);
return () => window.removeEventListener('wheel', handleZoomWheel, options); // Releases the listener if the user alt-tabs with Ctrl held — otherwise
// the keyup never fires and we stay in the slow path.
window.addEventListener('blur', unregisterZoomWheel);
return () => {
window.removeEventListener('keydown', handleZoomKeyDown);
window.removeEventListener('keyup', handleZoomKeyUp);
window.removeEventListener('blur', unregisterZoomWheel);
unregisterZoomWheel();
};
} }
// ============ Last.fm Session ============ // ============ Last.fm Session ============