diff --git a/.changeset/witty-bears-fail.md b/.changeset/witty-bears-fail.md new file mode 100644 index 00000000..ab758210 --- /dev/null +++ b/.changeset/witty-bears-fail.md @@ -0,0 +1,14 @@ +--- +'@tanstack/virtual-core': minor +--- + +feat: add skipRemeasurementOnBackwardScroll option to reduce stuttering + +Adds new option to skip re-measuring already-cached items during backward scrolling. +This prevents scroll adjustments that conflict with the user's scroll direction, reducing stuttering in dynamic content like social feeds or chat messages. + +When enabled, cached measurements are reused during backward scroll while `isScrolling` is true. Layout settles correctly once scrolling stops. + +**Changes:** +- Added `skipRemeasurementOnBackwardScroll` option (default: `false`) +- Skip re-measurement in `_measureElement` when scrolling backward with cached items \ No newline at end of file diff --git a/docs/api/virtualizer.md b/docs/api/virtualizer.md index 4c8c064e..b9f209f3 100644 --- a/docs/api/virtualizer.md +++ b/docs/api/virtualizer.md @@ -270,6 +270,15 @@ This option enables wrapping ResizeObserver measurements in requestAnimationFram It helps prevent the "ResizeObserver loop completed with undelivered notifications" error by ensuring that measurements align with the rendering cycle. This can improve performance and reduce UI jitter, especially when resizing elements dynamically. However, since ResizeObserver already runs asynchronously, adding requestAnimationFrame may introduce a slight delay in measurements, which could be noticeable in some cases. If resizing operations are lightweight and do not cause reflows, enabling this option may not provide significant benefits. +### `skipRemeasurementOnBackwardScroll` + +```tsx +skipRemeasurementOnBackwardScroll: boolean +``` +When enabled, prevents re-measuring items that have already been measured during backward scrolling. +This reduces stuttering caused by scroll position adjustments that conflict with the user's scroll direction. +It is recommended to use this property when scrolling in situations where item heights change dynamically. + ## Virtualizer Instance The following properties and methods are available on the virtualizer instance: diff --git a/packages/virtual-core/src/index.ts b/packages/virtual-core/src/index.ts index 57a151c4..4304c686 100644 --- a/packages/virtual-core/src/index.ts +++ b/packages/virtual-core/src/index.ts @@ -348,6 +348,7 @@ export interface VirtualizerOptions< enabled?: boolean isRtl?: boolean useAnimationFrameWithResizeObserver?: boolean + skipRemeasurementOnBackwardScroll?: boolean } export class Virtualizer< @@ -447,6 +448,7 @@ export class Virtualizer< isRtl: false, useScrollendEvent: false, useAnimationFrameWithResizeObserver: false, + skipRemeasurementOnBackwardScroll: false, ...opts, } } @@ -879,6 +881,21 @@ export class Virtualizer< } if (node.isConnected) { + // Check if we should skip remeasuring during backward scroll + if ( + this.options.skipRemeasurementOnBackwardScroll && + this.scrollDirection === 'backward' && + this.isScrolling + ) { + const isAlreadyMeasured = this.itemSizeCache.has(key) + if (isAlreadyMeasured) { + // Skip remeasuring to prevent stuttering during backward scroll + // Use cached measurement instead + return + } + } + + // Measure and update size this.resizeItem(index, this.options.measureElement(node, entry, this)) } }