Skip to content

Investigation: virtualized-list memory leak (#3241)#3708

Draft
mattgperry wants to merge 2 commits intomainfrom
worktree-fix-issue-3241
Draft

Investigation: virtualized-list memory leak (#3241)#3708
mattgperry wants to merge 2 commits intomainfrom
worktree-fix-issue-3241

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

Status: draft — investigation only, no code fix applied

Refs #3241

The reporter says scrolling a virtualized list of motion.div items with
initial={{ opacity: 0 }} animate={{ opacity: 1 }} causes the Chrome DevTools
"DOM Nodes" counter to climb without falling, even after manual GC. Removing
initial/animate stops the growth.

What I did

  • Read the issue and the linked CodeSandbox URLs (nj8cqp). The sandbox
    source is gated behind Cloudflare's bot challenge — WebFetch,
    curl, and the CSB API endpoints all returned the challenge page, so I
    could not retrieve the original App.js.
  • Traced the unmount path for plain animated motion.div:
    • useMotionRef calls visualElement.unmount() when React passes
      null to the ref callback (packages/framer-motion/src/motion/utils/use-motion-ref.ts:39).
    • VisualElement.unmount() cancels both scheduled frame callbacks,
      runs valueSubscriptions cleanup (which calls value.stop() for
      owned MotionValues, in turn calling animation.stop()
      animation.cancel() for WAAPI), unmounts each feature
      (AnimationFeature.unmount()animationState.reset()), and
      nulls this.current (packages/motion-dom/src/render/VisualElement.ts:504-525).
    • For plain animate on a motion.div neither projection nor
      InViewFeature is enabled (packages/framer-motion/src/motion/features/definitions.ts),
      so those teardown paths aren't relevant.
  • Surveyed module-level collections that could retain DOM nodes
    (animationMaps, appearAnimationStore, observerCallbacks, frame-step
    queues). All are WeakMaps or are cleared per frame. The optimised
    appear store only populates when data-appear-id is present (SSR), so
    CRA-style sandboxes don't touch it.
  • Reviewed recent leak-related commits (#3178 release-visual-element,
    #3453 reduced-motion listener, #3381 React 19 cleanup, popchild-refs,
    drag-cleanup). The unmount path has been touched repeatedly since
    this issue was filed.
  • Existing test animate-prop.test.tsx > unmount cancels active animations
    already proves onAnimationComplete is not invoked after unmount.
  • Issue comment from contributor @rortan134 (2025-08-11): "This seems
    to be fixed? … snapshots don't seem to be hinting any leaks."

Where I got stuck

I cannot reliably reproduce the leak:

  • The JSDOM-based unit test environment doesn't run WAAPI, so cancel-on-unmount
    can't be observed there.
  • Cypress runs in Electron 80, where document.getAnimations() is
    unavailable and detached-DOM accounting requires Chrome DevTools'
    Performance Monitor — not addressable from a test runner.
  • Without the original sandbox source I can't tell whether the original
    reporter's leak was caused by a third-party virtualizer holding refs,
    by a bug since fixed, or by Chrome behaviour around recently-cancelled
    WAAPI animations.

Per feedback_no_repro_no_pr.md: not landing happy-path coverage that
can't fail without a fix. No source change is included here.

What this PR adds

A single manual reproduction page at
dev/react/src/tests/animate-virtualized-list-memory.tsx (route
?test=animate-virtualized-list-memory). It cycles a window of 10
motion.div items every 50ms, simulating the virtualized scroll
described in the issue. A maintainer can open it in Chrome with the
Performance Monitor visible to see whether the DOM Nodes counter
stabilises — useful for confirming whether the bug is still live before
landing a fix.

Suggested next step

Confirm reproducibility against current main using the harness
(or the original CodeSandbox if accessible). If memory stabilises,
close #3241 as already-fixed; if it grows, the harness gives a
controllable starting point that doesn't depend on a third-party
virtualizer or external sandbox.

mattgperry and others added 2 commits May 9, 2026 06:27
Adds a virtualized-list reproduction harness for the reported memory
leak when scrolling animated motion.div items. The page cycles through
windows of items every 50ms, remounting fresh motion.divs each time.

Memory growth must be observed manually via Chrome DevTools' Performance
Monitor (DOM Nodes counter); the JSDOM/Electron 80 environment used by
unit and Cypress tests doesn't surface the leak.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add 100 child divs per item (matching the original sandbox) and a
FinalizationRegistry that tracks every motion.div's lifecycle. The
harness exposes mounted / unmounted / still-alive counts on
window.__leakStats so a leak can be detected programmatically rather
than relying solely on Chrome's DOM Nodes counter.

Verified locally with Chromium + --js-flags=--expose-gc: after 1700+
mount/unmount cycles the still-alive count stays bounded at the
visible-item count plus a handful of GC stragglers, indicating the
original leak is no longer reproducible on main.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

[BUG] Memory leak when scrolling animated items in a virtualized list.

1 participant