Skip to content

[react-refresh] fix memory leak: only add root to helpersByRoot on mount#36382

Open
sleitor wants to merge 1 commit intofacebook:mainfrom
sleitor:fix-36379
Open

[react-refresh] fix memory leak: only add root to helpersByRoot on mount#36382
sleitor wants to merge 1 commit intofacebook:mainfrom
sleitor:fix-36379

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented May 1, 2026

Summary

Fixes #36379

Problem

helpersByRoot.set(root, helpers) was called unconditionally on every onCommitFiberRoot invocation. This means that even after a root is unmounted (and correctly deleted from helpersByRoot in the wasMounted && !isMounted branch), any subsequent commit from that renderer ID would re-add the root to the Map.

This causes memory leaks for secondary renderers such as react-three-fiber, where Canvas roots are repeatedly mounted and unmounted. The helpersByRoot Map grows without bound, preventing garbage collection of fiber roots and their associated component trees.

Fix

Move helpersByRoot.set(root, helpers) into the mount branches only:

  1. !wasMounted && isMounted (new mount with an existing alternate): set the root in helpersByRoot.
  2. else (first mount without alternate): set the root in helpersByRoot.
  3. wasMounted && isMounted (update): no change needed — root is already in the Map from the initial mount.
  4. !wasMounted && !isMounted && didError: guard failedRoots.add(root) with helpersByRoot.has(root) to avoid retaining roots that were never successfully mounted.

Testing

  • Existing tests in ReactFreshMultipleRenderer-test.internal.js exercise the multi-renderer path and should continue to pass.
  • The reporter provided a repro repo with WeakRef visualization showing the leak before and after the fix.

helpersByRoot.set(root, helpers) was called unconditionally on every
onCommitFiberRoot invocation, including re-renders after unmount. This
caused secondary renderer roots (e.g. react-three-fiber Canvas) to
remain in the helpersByRoot Map permanently, preventing GC.

Fix:
- Move helpersByRoot.set(root, helpers) into the mount branches only
  (!wasMounted && isMounted, and the no-alternate initial mount path).
- Skip helpersByRoot.set for the update case (wasMounted && isMounted)
  since the root is already in the Map from the initial mount.
- Guard failedRoots.add in the (!wasMounted && !isMounted && didError)
  case with helpersByRoot.has(root) to avoid retaining roots that were
  never successfully mounted.

Fixes facebook#36379
@meta-cla meta-cla Bot added the CLA Signed label May 1, 2026
@react-sizebot
Copy link
Copy Markdown

Comparing: f4e0d4e...e204ed5

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 = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 612.85 kB 612.85 kB = 108.29 kB 108.29 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 678.78 kB 678.78 kB = 119.26 kB 119.26 kB
facebook-www/ReactDOM-prod.classic.js = 699.20 kB 699.20 kB = 122.82 kB 122.82 kB
facebook-www/ReactDOM-prod.modern.js = 689.51 kB 689.51 kB = 121.21 kB 121.21 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactFreshRuntime-dev.classic.js +1.18% 12.34 kB 12.49 kB +0.81% 2.97 kB 3.00 kB
facebook-www/ReactFreshRuntime-dev.modern.js +1.18% 12.34 kB 12.49 kB +0.81% 2.97 kB 3.00 kB
oss-experimental/react-refresh/cjs/react-refresh-runtime.development.js +1.17% 12.36 kB 12.50 kB +0.84% 2.98 kB 3.01 kB
oss-stable-semver/react-refresh/cjs/react-refresh-runtime.development.js +1.17% 12.36 kB 12.50 kB +0.84% 2.98 kB 3.01 kB
oss-stable/react-refresh/cjs/react-refresh-runtime.development.js +1.17% 12.36 kB 12.50 kB +0.84% 2.98 kB 3.01 kB

Generated by 🚫 dangerJS against e204ed5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: react-refresh leaks FiberRootNodes from secondary renderers

2 participants