fix(feed): read further ahead so fast scrolling doesn't stall at "Load more"
Keyset pagination is strictly serial (a page needs the previous page's cursor) and the read-ahead only fired within a fixed 4 rows of the loaded end, pulling one 60-item page per cycle. A fast free-spin flick (MX Master & co.) out-scrolls that tiny lookahead, lands on an empty bottom, and waits out a chain of serial round-trips — the multi-second "Load more" hang. Scale the lookahead to the rendered row count (PREFETCH_VIEWPORTS screenfuls); since the effect re-runs when a page settles it chains, keeping a couple of screens buffered ahead of the fold so the content is usually already there. Bounded, and a no-op for finite lists.
This commit is contained in:
@@ -23,9 +23,17 @@ import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
// (grid-cols-[repeat(auto-fill,minmax(260px,1fr))] with a gap-4 / 1rem gutter).
|
||||
const GRID_GAP = 16;
|
||||
const LIST_GAP = 4;
|
||||
// Start fetching the next page this many rows before the end scrolls into view
|
||||
// (keeps the old IntersectionObserver's generous prefetch feel).
|
||||
// Infinite-scroll read-ahead. The next page is pulled once the last rendered row comes within
|
||||
// `lookahead` rows of the loaded end, where lookahead = max(PREFETCH_ROWS, rendered-rows ×
|
||||
// PREFETCH_VIEWPORTS). Sizing it to the number of rows the viewport currently renders makes it adapt
|
||||
// to window height / column count / row height, and keeps ~PREFETCH_VIEWPORTS screenfuls buffered
|
||||
// below the fold. The fixed floor is the finite-list / first-paint case.
|
||||
const PREFETCH_ROWS = 4;
|
||||
// Keep this many screenfuls of rows loaded ahead of the fold. A fast free-spin flick (MX Master &
|
||||
// co.) out-scrolls a mere 4-row lookahead and lands on an empty bottom, then waits out serial keyset
|
||||
// round-trips — the "Load more" hangs for seconds. Buffering a couple of screens ahead means the
|
||||
// content is usually already there when the user gets to it.
|
||||
const PREFETCH_VIEWPORTS = 2;
|
||||
|
||||
/** Columns that fit `width`, or 1 for the single-column layout (`minCol == null`). */
|
||||
function columnsFor(minCol: number | null, width: number): number {
|
||||
@@ -140,13 +148,17 @@ export default function VirtualGrid<T>({
|
||||
|
||||
const virtualRows = virtualizer.getVirtualItems();
|
||||
|
||||
// Infinite-scroll trigger: once the last rendered row is within PREFETCH_ROWS of the end, pull
|
||||
// the next page. A no-op for finite lists (no fetchNextPage / hasNextPage false).
|
||||
// Infinite-scroll read-ahead: once the last rendered row is within `lookahead` rows of the end,
|
||||
// pull the next page. A no-op for finite lists (no fetchNextPage / hasNextPage false). Because this
|
||||
// effect re-runs when a page settles (isFetchingNextPage flips false) it CHAINS — refilling the
|
||||
// buffer page by page as the user consumes it, so a fast scroll reaches the loaded end far less
|
||||
// often. See PREFETCH_VIEWPORTS above for why the lookahead scales with the rendered row count.
|
||||
useEffect(() => {
|
||||
if (!fetchNextPage) return;
|
||||
if (!fetchNextPage || !hasNextPage || isFetchingNextPage) return;
|
||||
const last = virtualRows[virtualRows.length - 1];
|
||||
if (!last) return;
|
||||
if (last.index >= rows.length - 1 - PREFETCH_ROWS && hasNextPage && !isFetchingNextPage) {
|
||||
const lookahead = Math.max(PREFETCH_ROWS, virtualRows.length * PREFETCH_VIEWPORTS);
|
||||
if (last.index >= rows.length - 1 - lookahead) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [virtualRows, rows.length, hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
Reference in New Issue
Block a user