Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const virtualizer = createVirtualizer({
getItemKey: (index) => rows[index]!.id,
initialViewportSize: 600,
overscan: 5,
overscanPixels: 600,
});

virtualizer.setViewport(4_800, 600);
Expand All @@ -30,6 +31,11 @@ Each virtual item contains its `index`, stable `key`, `start`, `size`, and
`end`. Render the returned items inside a spacer with `totalSize` on the main
scrolling axis.

`overscan` adds an item-count buffer, while `overscanPixels` adds a scroll-axis
pixel buffer on both sides of the viewport. A pixel buffer of roughly one
viewport helps framework renderers stay ahead of high-velocity native
scrolling, especially when item heights vary.

## Dynamic measurement

Supply measured sizes after rendering:
Expand Down
38 changes: 36 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export interface VirtualizerOptions<TKey extends VirtualItemKey = number> {
readonly initialOffset?: number;
readonly initialViewportSize?: number;
readonly overscan?: number;
/**
* Additional render buffer in scroll-axis pixels on both sides of the
* viewport. Useful for preventing native scrolling from outrunning a
* framework renderer.
*/
readonly overscanPixels?: number;
readonly paddingEnd?: number;
readonly paddingStart?: number;
}
Expand Down Expand Up @@ -56,6 +62,7 @@ interface ResolvedOptions<TKey extends VirtualItemKey> {
readonly estimateSize: (index: number) => number;
readonly getItemKey: (index: number) => TKey;
readonly overscan: number;
readonly overscanPixels: number;
readonly paddingEnd: number;
readonly paddingStart: number;
}
Expand Down Expand Up @@ -97,6 +104,10 @@ function resolveOptions<TKey extends VirtualItemKey>(
getItemKey:
options.getItemKey ?? (defaultKey as unknown as (index: number) => TKey),
overscan: validateOverscan(options.overscan ?? 1),
overscanPixels: validateNonNegativeFinite(
options.overscanPixels ?? 0,
"overscanPixels",
),
paddingEnd: validateNonNegativeFinite(
options.paddingEnd ?? 0,
"paddingEnd",
Expand Down Expand Up @@ -497,11 +508,34 @@ export class Virtualizer<TKey extends VirtualItemKey = number> {
visibleStart + 1,
this.#sizes.countStartingBefore(relativeEnd),
);
const renderStart = Math.max(0, visibleStart - this.#options.overscan);
const renderEndExclusive = Math.min(
let renderStart = Math.max(0, visibleStart - this.#options.overscan);
let renderEndExclusive = Math.min(
this.#options.count,
visibleEndExclusive + this.#options.overscan,
);
if (this.#options.overscanPixels > 0) {
const pixelRenderStart = Math.max(
0,
relativeStart - this.#options.overscanPixels,
);
const pixelRenderEnd = Math.min(
this.#sizes.total,
relativeEnd + this.#options.overscanPixels,
);
const pixelRenderStartIndex = Math.min(
this.#options.count - 1,
this.#sizes.countEndingAtOrBefore(pixelRenderStart),
);
const pixelRenderEndExclusive = Math.max(
pixelRenderStartIndex + 1,
this.#sizes.countStartingBefore(pixelRenderEnd),
);
renderStart = Math.min(renderStart, pixelRenderStartIndex);
renderEndExclusive = Math.max(
renderEndExclusive,
pixelRenderEndExclusive,
);
}
const items: VirtualItem<TKey>[] = [];

for (let index = renderStart; index < renderEndExclusive; index += 1) {
Expand Down
23 changes: 23 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ describe("Virtualizer", () => {
});
});

it("combines item and pixel overscan", () => {
const virtualizer = createDefault({
initialOffset: 200,
overscan: 1,
overscanPixels: 60,
});

expect(virtualizer.getSnapshot().items.map((item) => item.index)).toEqual([
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
]);
expect(virtualizer.getSnapshot().visibleRange).toEqual({
endIndex: 14,
startIndex: 10,
});
});

it("accounts for item and outer padding", () => {
const virtualizer = createDefault({
count: 3,
Expand Down Expand Up @@ -292,6 +308,13 @@ describe("Virtualizer", () => {
overscan: -1,
}),
).toThrow("overscan must be a non-negative safe integer");
expect(() =>
createVirtualizer({
count: 1,
estimateSize: () => 20,
overscanPixels: Number.NaN,
}),
).toThrow("overscanPixels must be a non-negative finite number");
expect(() =>
createVirtualizer({
count: 1,
Expand Down
Loading