Skip to content

fix(virtual-core): notify synchronously after above-viewport resize compensation#1239

Merged
piecyk merged 1 commit into
TanStack:mainfrom
piecyk:damian/fix/up-scroll-compensation-flash
Jul 22, 2026
Merged

fix(virtual-core): notify synchronously after above-viewport resize compensation#1239
piecyk merged 1 commit into
TanStack:mainfrom
piecyk:damian/fix/up-scroll-compensation-flash

Conversation

@piecyk

@piecyk piecyk commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #1227 — the viewport flashes/jumps for one frame when above-viewport rows resize while the user scrolls up (dynamic measurement, real heights differ from estimateSize).

Diagnosis

The reporter's escape hatch (assigning shouldAdjustScrollPositionOnItemSizeChange on the instance) works — the callback fires for every backward re-measure and the compensation branch runs. Instrumenting the repro showed the compensation is also numerically correct:

i=100 delta=80 end=11870 tracked=13086 dom=13086 skew=0 dir=backward
i=99  delta=80 end=11790 tracked=13003 dom=13003 skew=0 dir=backward
...
  • skew = dom − tracked = 0 on every fire → scrollTo writes off the correct live base (no stale-offset bug).
  • The scrollbar stays anchored and the final resting offset is exact → delta is applied once, right direction.
  • overflow-anchor: none changes nothing → not native browser scroll anchoring.

What remains is a single-frame paint flash. resizeItem writes scrollTop synchronously inside the ResizeObserver callback (applyScrollAdjustment), but then notified asynchronously — the grown item transforms commit on a later render. ResizeObserver fires right before paint, so the browser can paint one frame with the new scrollTop and the old transforms: content jerks by delta and snaps back on the next commit. This is the inverse of #1237 (grow the container before syncing scroll); pre-3.16 didn't decouple the two.

Fix

When a compensation actually moves the scroll position, notify synchronously so the transform commit lands in the same ResizeObserver callback as the scrollTop write — the browser paints once, consistently. applyScrollAdjustment now returns whether it wrote scrollTop this tick; resizes that move nothing (below-fold measurements) and iOS-deferred adjustments keep the cheaper async notify.

Tests

Note

The synchronous flush relies on the React adapter's useFlushSync (default true). Consumers who set useFlushSync: false opt out of synchronous flushing, so the flash can return for them — left as-is rather than overriding an explicit choice.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a brief one-frame viewport jump when scrolling upward while above-viewport items resize.
    • Improved scroll anchoring so resize compensation updates visual transforms and scroll position in the same paint when a scroll write occurs.
    • Kept updates efficient when compensation doesn’t change scroll position, using the lighter asynchronous path.
  • Tests
    • Added regression coverage for above-viewport scroll writes and the asynchronous case when no scroll-position adjustment is needed.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c0b06b4-f5e3-4561-8b3c-bd38c88cd665

📥 Commits

Reviewing files that changed from the base of the PR and between 54bb501 and a8a35b1.

📒 Files selected for processing (3)
  • .changeset/fix-up-scroll-compensation-flash.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

📝 Walkthrough

Walkthrough

Virtualizer now reports whether resize compensation synchronously writes scroll position. resizeItem uses that result to choose synchronous or asynchronous notifications, with tests covering both paths and a patch changeset documenting the fix.

Changes

Scroll compensation

Layer / File(s) Summary
Report synchronous scroll adjustments
packages/virtual-core/src/index.ts
applyScrollAdjustment returns whether it performed an immediate scroll write, distinguishing zero-delta, iOS-deferred, and non-iOS adjustments.
Select notification timing for resized items
packages/virtual-core/src/index.ts, packages/virtual-core/tests/index.test.ts, .changeset/fix-up-scroll-compensation-flash.md
resizeItem passes the adjustment result to notify; tests verify synchronous notification when scroll position changes and asynchronous notification otherwise.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant resizeItem
  participant applyScrollAdjustment
  participant scrollToFn
  participant onChange
  resizeItem->>applyScrollAdjustment: Apply resize compensation
  applyScrollAdjustment->>scrollToFn: Write adjusted scroll position when applicable
  applyScrollAdjustment-->>resizeItem: Return synchronous-write status
  resizeItem->>onChange: Notify with sync status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and omits the checklist and release-impact sections. Reformat it to the repository template and add the checklist plus release-impact sections, including the testing and changeset status.
Linked Issues check ⚠️ Warning The PR fixes notification timing, but #1227 also requires working above-viewport compensation during upward scroll and a functioning opt-out, which remain unaddressed. Address the original backward-scroll compensation bug and wire shouldAdjustScrollPositionOnItemSizeChange from options so the documented escape hatch works.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: synchronous notification after above-viewport resize compensation.
Out of Scope Changes check ✅ Passed The changes stay focused on the resize-compensation fix, regression tests, and the accompanying changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jul 22, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit a8a35b1

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 58s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 19s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-22 11:30:10 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

npm i https://pkg.pr.new/@tanstack/angular-virtual@1239

@tanstack/lit-virtual

npm i https://pkg.pr.new/@tanstack/lit-virtual@1239

@tanstack/marko-virtual

npm i https://pkg.pr.new/@tanstack/marko-virtual@1239

@tanstack/react-virtual

npm i https://pkg.pr.new/@tanstack/react-virtual@1239

@tanstack/solid-virtual

npm i https://pkg.pr.new/@tanstack/solid-virtual@1239

@tanstack/svelte-virtual

npm i https://pkg.pr.new/@tanstack/svelte-virtual@1239

@tanstack/virtual-core

npm i https://pkg.pr.new/@tanstack/virtual-core@1239

@tanstack/vue-virtual

npm i https://pkg.pr.new/@tanstack/vue-virtual@1239

commit: a8a35b1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/virtual-core/tests/index.test.ts (1)

2741-2766: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the reported upward-scroll state.

This test leaves isScrolling=false and scrollDirection=null, so it only validates compensation while idle. Use the helper’s existing emitScroll with a decreasing offset before resizeItem; otherwise, a regression affecting active upward scrolling could still pass.

Suggested test adjustment
-  const { v, scrollToFn } = createAdjustmentVirtualizer({
+  const { v, scrollToFn, emitScroll } = createAdjustmentVirtualizer({
     offset: 600,
     onChange,
   })
   v['getMeasurements']()
+  emitScroll(500, true)
   onChange.mockClear()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/virtual-core/tests/index.test.ts` around lines 2741 - 2766, Update
the test around createAdjustmentVirtualizer to exercise active upward scrolling:
use the helper’s existing emitScroll with a decreasing offset before calling
resizeItem, while preserving the current compensation and synchronous onChange
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/virtual-core/tests/index.test.ts`:
- Around line 2741-2766: Update the test around createAdjustmentVirtualizer to
exercise active upward scrolling: use the helper’s existing emitScroll with a
decreasing offset before calling resizeItem, while preserving the current
compensation and synchronous onChange assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 392c9e81-ae96-40df-b91a-961aaa5729c8

📥 Commits

Reviewing files that changed from the base of the PR and between ef6e92a and 54bb501.

📒 Files selected for processing (3)
  • .changeset/fix-up-scroll-compensation-flash.md
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

…ompensation

`resizeItem` writes `scrollTop` synchronously inside the ResizeObserver
callback to compensate for an above-viewport size change, but then notified
asynchronously. The browser could paint one frame with the new `scrollTop`
and the old item transforms, so the viewport visibly jumped by the resize
delta and snapped back on the next commit (TanStack#1227) — the scrollbar stayed
anchored (skew=0) but the content flashed.

When a compensation actually moves the scroll position, notify synchronously
so the transform commit lands in the same paint as the scroll write. Resizes
that move nothing (below-fold measurements) and iOS-deferred adjustments keep
the cheaper async notify. `applyScrollAdjustment` now reports whether it wrote
`scrollTop` this tick to drive that decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@piecyk
piecyk force-pushed the damian/fix/up-scroll-compensation-flash branch from 54bb501 to a8a35b1 Compare July 22, 2026 11:26
@piecyk
piecyk merged commit a5417b4 into TanStack:main Jul 22, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Up-scroll jank from uncompensated above-viewport resizes

1 participant