From a9893068cdc01d6ebdf763635bb9f055d0519fe0 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 5 Sep 2026 15:15:44 +0000 Subject: [PATCH] Don't re-anchor the grid on height-only viewport changes The settle window I added for rotation also ran for every `resize`, and on a phone the URL bar collapsing during a fast flick is exactly that. It re-asserted an anchor captured before the flick, so a scrollTo landed mid-momentum and stopped the scroll dead. Only a width change (or an orientationchange) can move a card: positions are absolute pixels in the grid's own space, so a height-only resize leaves both the layout and the scroll position correct and needs no restore at all. The one exception kept is a scrollbar appearing on desktop, which changes the grid's inner width while window.innerWidth stays put -- that re-packs, but only when the columns actually moved. Modelled the failure in headless Chrome (height change mid-flick, scroll continuing): the flick was yanked back 173px before, and now runs on to where it was headed. Rotation still re-anchors, and a fast scroll with 80 videos loaded holds 60fps (median 16.7ms/frame, max 17.2ms, no frame over 32ms). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd --- frontend/js/videos.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/frontend/js/videos.js b/frontend/js/videos.js index 6c521b3..2711902 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -866,8 +866,33 @@ App.videos = App.videos || {}; return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable; }; - const beginSettle = function() { + let settleWidth = 0; + + const beginSettle = function(event) { if (isTypingTarget()) return; + // Only a width change moves cards: their positions are absolute + // pixels in the grid's own space, so a height-only resize -- the + // mobile URL bar collapsing as you scroll, most of all -- leaves the + // layout (and therefore the scroll position) exactly right. Settling + // on those was actively harmful: a URL-bar resize landing mid-flick + // re-asserted an anchor captured before the flick and yanked the + // page back, killing the momentum scroll. + const width = window.innerWidth || 0; + const widthChanged = width !== settleWidth; + settleWidth = width; + const rotating = !!(event && event.type === 'orientationchange'); + if (!widthChanged && !rotating) { + // One exception: a scrollbar appearing/disappearing changes the + // grid's inner width while window.innerWidth stays put. Re-pack + // if the columns really moved (relayout keeps the reader's + // place); otherwise leave the scroll completely alone. + const prevCols = cols; + const prevColWidth = colWidth; + if (measureMetrics() && (cols !== prevCols || Math.abs(colWidth - prevColWidth) > 0.5)) { + relayout(); + } + return; + } if (!heldAnchor) heldAnchor = lastAnchor; settleTimers.forEach(clearTimeout); settleTimers = SETTLE_TICKS.map((ms) => setTimeout(settleTick, ms)); @@ -878,6 +903,7 @@ App.videos = App.videos || {}; if (!cols) measureMetrics(); if (initialized) return; initialized = true; + settleWidth = window.innerWidth || 0; window.addEventListener('scroll', scheduleUpdate, { passive: true }); // orientationchange fires first (before the viewport metrics change), // which is exactly when the pre-rotation anchor is still valid.