Skip to content

[DOM] Unobserve fragment IntersectionObserver targets after exit - #37408

Merged
jackpope merged 2 commits into
react:mainfrom
jackpope:fr-unobserve-cleanup
Aug 31, 2026
Merged

[DOM] Unobserve fragment IntersectionObserver targets after exit#37408
jackpope merged 2 commits into
react:mainfrom
jackpope:fr-unobserve-cleanup

Conversation

@jackpope

@jackpope jackpope commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Fragment IntersectionObserver targets used to stay observed after a child was removed so the exit record (isIntersecting: false) could still fire, but that left the observer holding detached nodes. We now unobserve ResizeObserver targets immediately, and delay IntersectionObserver unobserve until after paint so the exit still lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending unobserve so a later cleanup does not detach a child that’s still visible.

Closes #37452
Closes #37302
Fixes #37451

…record

Keep deleted first-level children observed until after paint so disconnect still delivers isIntersecting: false, then drop the observer's strong refs. ResizeObserver targets are unobserved immediately.
@meta-cla meta-cla Bot added the CLA Signed label Aug 29, 2026
@jackpope
jackpope requested a review from eps1lon August 29, 2026 23:07
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

Comparing: 2dc7da7...c5620ec

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 7.35 kB 7.35 kB = 1.96 kB 1.96 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.38% 622.23 kB 624.62 kB +0.33% 110.01 kB 110.38 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 7.36 kB 7.36 kB +0.05% 1.96 kB 1.96 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.34% 693.45 kB 695.84 kB +0.29% 121.46 kB 121.81 kB
facebook-www/ReactDOM-prod.classic.js +0.29% 713.62 kB 715.69 kB +0.24% 124.94 kB 125.24 kB
facebook-www/ReactDOM-prod.modern.js +0.29% 703.93 kB 706.00 kB +0.24% 123.36 kB 123.67 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/react-dom/cjs/react-dom-client.production.js +0.38% 622.11 kB 624.50 kB +0.33% 109.99 kB 110.35 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.38% 622.23 kB 624.62 kB +0.33% 110.01 kB 110.38 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-prod.js +0.36% 661.64 kB 664.03 kB +0.32% 116.16 kB 116.54 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-prod.js +0.36% 667.75 kB 670.14 kB +0.32% 117.38 kB 117.75 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.34% 693.45 kB 695.84 kB +0.29% 121.46 kB 121.81 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.profiling.js +0.34% 696.86 kB 699.25 kB +0.29% 120.96 kB 121.31 kB
oss-stable/react-dom/cjs/react-dom-profiling.profiling.js +0.34% 696.98 kB 699.37 kB +0.29% 120.99 kB 121.34 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +0.34% 707.86 kB 710.25 kB +0.28% 124.98 kB 125.33 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-profiling.js +0.32% 737.20 kB 739.59 kB +0.29% 126.91 kB 127.28 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-profiling.js +0.32% 743.75 kB 746.13 kB +0.29% 128.19 kB 128.56 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +0.31% 772.95 kB 775.34 kB +0.28% 133.14 kB 133.51 kB
facebook-www/ReactDOM-prod.modern.js +0.29% 703.93 kB 706.00 kB +0.24% 123.36 kB 123.67 kB
facebook-www/ReactDOM-prod.classic.js +0.29% 713.62 kB 715.69 kB +0.24% 124.94 kB 125.24 kB
facebook-www/ReactDOMTesting-prod.modern.js +0.29% 718.33 kB 720.40 kB +0.23% 126.94 kB 127.24 kB
facebook-www/ReactDOMTesting-prod.classic.js +0.28% 728.02 kB 730.09 kB +0.23% 128.50 kB 128.80 kB
facebook-www/ReactDOM-profiling.modern.js +0.26% 782.17 kB 784.24 kB +0.22% 134.29 kB 134.59 kB
facebook-www/ReactDOM-profiling.classic.js +0.26% 790.31 kB 792.39 kB +0.22% 135.57 kB 135.87 kB
oss-stable-semver/react-dom/cjs/react-dom-client.development.js +0.23% 1,177.97 kB 1,180.69 kB +0.22% 197.40 kB 197.83 kB
oss-stable/react-dom/cjs/react-dom-client.development.js +0.23% 1,178.10 kB 1,180.81 kB +0.22% 197.43 kB 197.86 kB
oss-stable-semver/react-dom/cjs/react-dom-profiling.development.js +0.23% 1,195.10 kB 1,197.82 kB +0.22% 200.36 kB 200.80 kB
oss-stable/react-dom/cjs/react-dom-profiling.development.js +0.23% 1,195.23 kB 1,197.95 kB +0.22% 200.39 kB 200.82 kB
facebook-react-native/react-dom/cjs/ReactDOMClient-dev.js +0.22% 1,236.95 kB 1,239.67 kB +0.21% 206.36 kB 206.80 kB
facebook-react-native/react-dom/cjs/ReactDOMProfiling-dev.js +0.22% 1,253.97 kB 1,256.69 kB +0.22% 209.28 kB 209.75 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js +0.21% 1,275.87 kB 1,278.59 kB +0.19% 212.57 kB 212.97 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.21% 1,292.42 kB 1,295.14 kB +0.19% 216.18 kB 216.59 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js +0.21% 1,292.94 kB 1,295.66 kB +0.19% 215.46 kB 215.88 kB

Generated by sizebot against c5620ec

@github-actions github-actions Bot added the React Core Team Opened by a member of the React Core Team label Aug 29, 2026

@eps1lon eps1lon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There's also reported RN leakage in #37302. Are we ignoring that for now?

Comment thread packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js Outdated
Comment thread packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js Outdated
@jackpope

Copy link
Copy Markdown
Collaborator Author

@eps1lon I'm not sure how to wire up in Fabric yet, requestPostPaintCallback is a noop there https://github.com/react/react/blob/main/packages/react-native-renderer/src/ReactFiberConfigFabric.js#L614

@rubennorte is it possible to use requestAnimationFrame from ReactFiberConfigFabric?

@eps1lon

eps1lon commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

so the exit record (isIntersecting: false) could still fire

What is the downside of not doing that (i.e. just unobserve immediately).

@jackpope

Copy link
Copy Markdown
Collaborator Author

@eps1lon if you are tracking the state of a list of children to understand if all/none/some are in view, a deleted child that is unobserved immediately doesn't get the exit record so you'd continue to track the item as intersecting forever.

@jackpope
jackpope merged commit 065bc84 into react:main Aug 31, 2026
239 checks passed
@jackpope
jackpope deleted the fr-unobserve-cleanup branch August 31, 2026 17:13
github-actions Bot pushed a commit that referenced this pull request Aug 31, 2026
)

Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.

Closes #37452
Closes #37302
Fixes #37451

DiffTrain build for [065bc84](065bc84)
github-actions Bot pushed a commit that referenced this pull request Aug 31, 2026
)

Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.

Closes #37452
Closes #37302
Fixes #37451

DiffTrain build for [065bc84](065bc84)
github-actions Bot pushed a commit to code/lib-react that referenced this pull request Aug 31, 2026
…ct#37408)

Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.

Closes react#37452
Closes react#37302
Fixes react#37451

DiffTrain build for [065bc84](react@065bc84)
github-actions Bot pushed a commit to code/lib-react that referenced this pull request Aug 31, 2026
…ct#37408)

Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.

Closes react#37452
Closes react#37302
Fixes react#37451

DiffTrain build for [065bc84](react@065bc84)
github-actions Bot pushed a commit to srpatcha/react that referenced this pull request Sep 1, 2026
…ct#37408)

Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.

Closes react#37452
Closes react#37302
Fixes react#37451

DiffTrain build for [065bc84](react@065bc84)
github-actions Bot pushed a commit to srpatcha/react that referenced this pull request Sep 1, 2026
…ct#37408)

Fragment IntersectionObserver targets used to stay observed after a
child was removed so the exit record (isIntersecting: false) could still
fire, but that left the observer holding detached nodes. We now
unobserve ResizeObserver targets immediately, and delay
IntersectionObserver unobserve until after paint so the exit still
lands, then drop the strong ref.

If the same node is reinserted before that flush, we cancel the pending
unobserve so a later cleanup does not detach a child that’s still
visible.

Closes react#37452
Closes react#37302
Fixes react#37451

DiffTrain build for [065bc84](react@065bc84)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fragment observers retain removed child elements

2 participants