From b0e2d8f556b3bbcda812fa09b69eb2b8f9f70c65 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Mon, 13 Jul 2026 13:24:09 +0200 Subject: [PATCH] feat: add minPageSize option to control query page size --- README.md | 22 ++++++++++++++++++++++ src/core/virtualizer.test.ts | 17 +++++++++++++++++ src/core/virtualizer.ts | 28 ++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 272b1b9..d2443d7 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,28 @@ getPageQuery: ({limit, start, dir, settled}) => ({ }), ``` +### Page size + +The `limit` passed to `getPageQuery` comes from the page size: about three +viewports' worth of rows at `estimateSize`, but never below the `minPageSize` +floor (default 100). The floor is sized for short rows; for tall rows (cards, +comments) 100 rows is many viewports of content, so the floor dominates the +formula and each page load renders far more DOM than needed — showing up as +long tasks when a page lands mid-scroll. Lower it so page size tracks the +viewport again: + +```ts +useZeroVirtualizer({ + estimateSize: () => 200, // tall cards + minPageSize: 20, + // ... +}); +``` + +Smaller pages trade a few more query round trips for smaller, smoother +per-page render bursts. The page size never shrinks once grown, and is rounded +up to an even number (paging splits pages in half around permalinks). + ### Scroll settling `useZeroVirtualizer` tracks whether the user has stopped scrolling: diff --git a/src/core/virtualizer.test.ts b/src/core/virtualizer.test.ts index 470cf00..b3fb207 100644 --- a/src/core/virtualizer.test.ts +++ b/src/core/virtualizer.test.ts @@ -409,3 +409,20 @@ describe('ZeroVirtualizer wrapper contract', () => { }); }); }); + +describe('minPageSize', () => { + test('defaults the query page size to 100', () => { + const v = new ZeroVirtualizer(makeOptions()); + expect(v.getQueryInputs().pageSize).toBe(100); + }); + + test('lowers the floor for tall rows', () => { + const v = new ZeroVirtualizer(makeOptions({minPageSize: 20})); + expect(v.getQueryInputs().pageSize).toBe(20); + }); + + test('is rounded up to an even number (paging halves pages)', () => { + const v = new ZeroVirtualizer(makeOptions({minPageSize: 15})); + expect(v.getQueryInputs().pageSize).toBe(16); + }); +}); diff --git a/src/core/virtualizer.ts b/src/core/virtualizer.ts index bcde3d1..267352e 100644 --- a/src/core/virtualizer.ts +++ b/src/core/virtualizer.ts @@ -119,6 +119,14 @@ export type VirtualizerOptions = { count?: number | undefined; permalinkID?: string | null | undefined; settleTime?: number | undefined; + /** + * Floor for the query page size. The page size is derived from the viewport + * (about three viewports' worth of rows at `estimateSize`), but never drops + * below this floor. Defaults to 100 — sized for short rows; lower it for + * tall rows (cards, comments) where 100 rows is many viewports of content. + * Rounded up to an even number (paging halves pages around permalinks). + */ + minPageSize?: number | undefined; scrollState?: ScrollHistoryState | null | undefined; onScrollStateChange?: | ((state: ScrollHistoryState) => void) @@ -233,7 +241,9 @@ export class ZeroVirtualizer { // ---- paging state --------------------------------------------------------- #paging: PagingState; - #pageSize = MIN_PAGE_SIZE; + // Assigned from #minPageSize() in the constructor (field initializers run + // before #options is set); grows monotonically in #updatePageSize. + #pageSize: number; #settled = false; // ---- change propagation --------------------------------------------------- @@ -318,6 +328,7 @@ export class ZeroVirtualizer { ) { this.#resolveScrollElement = resolveScrollElement; this.#options = options; + this.#pageSize = this.#minPageSize(); // Initialize paging directly from the restorable state so the first // render already queries the right window (this also survives React // Strict Mode's double construction — the constructor is pure). @@ -1003,16 +1014,21 @@ export class ZeroVirtualizer { // ---- rows/options-driven transitions (afterDOMUpdate) ----------------------- + // The configured page-size floor: `minPageSize` (evened — paging halves + // pages around permalinks), defaulting to MIN_PAGE_SIZE. + #minPageSize(): number { + const min = this.#options.minPageSize; + return min === undefined ? MIN_PAGE_SIZE : makeEven(Math.max(2, min)); + } + #updatePageSize(): void { + const min = this.#minPageSize(); const el = this.#el; const height = el ? this.#viewportRect(el).height : 0; const newPageSize = height > 0 - ? Math.max( - MIN_PAGE_SIZE, - makeEven(Math.ceil(height / this.#rowEstimate()) * 3), - ) - : MIN_PAGE_SIZE; + ? Math.max(min, makeEven(Math.ceil(height / this.#rowEstimate()) * 3)) + : min; if (newPageSize > this.#pageSize) { this.#pageSize = newPageSize; this.#version++;