Skip to content

perf: stop leaking React trees on repeated injection mutations - #12436

Merged
guanbinrui merged 2 commits into
developfrom
perf/injection-react-tree-leaks
Sep 5, 2026
Merged

perf: stop leaking React trees on repeated injection mutations#12436
guanbinrui merged 2 commits into
developfrom
perf/injection-react-tree-leaks

Conversation

@guanbinrui

Copy link
Copy Markdown
Member

Summary

Follow-up to #12432, which fixed this class of React-tree leak for injectAvatar and the tips buttons on x.com. The remaining injection points that build a React tree inside an onNodeMutation / onTargetChanged handler had the same bug: a mutation of the watched element (lazy image src swaps, hover states, feed virtualization reusing nodes) re-runs the handler, which attaches a fresh shadow-root sibling + React tree without tearing down the previous one. Over a long scroll session this accumulates orphaned DOM nodes / shadow roots / React roots — each with its own data-fetching hooks — until navigation.

Changes

Area Fix
facebook / minds / instagram injection/Avatar/index.tsx Call remove() before re-creating the DOMProxy, mirroring the twitter.com fix from #12432.
twitter.com/injection/MaskIcon.tsxinjectMaskIconToPostTwitter post.author refines several times as parsing completes and re-fires on every onNodeMutation of the tweet, so each add() stacked another DOMProxy shadow sibling + <Icon> React root (visible as duplicated Mask icons on posts). Reuse a single proxy, tear down the previous tree first, and release the post.author subscription on signal abort.
twitter.com/injection/MaskIcon.tsxinjectMaskUserBadgeAtTwitter (_ helper) check() re-ran on every mutation and re-called attachReactTreeWithContainer with the same shadow key, which the mounter rejects with a console.error on every floating-bio-card hover while also clobbering remover to a no-op. Attach once; remove() / onRemove reset the guard so a genuinely re-added element still re-attaches.
site-adaptor-infra/ui.tsstartPostListener The per-post AbortController wired a listener onto the page-lifetime signal on every post set and never removed it, so every post scrolled past left a permanent listener (and its retained controller). Pass { signal: abort.signal } so it drops when the post unmounts.

Behavior

Output values are unchanged; only the redundant / leaked work is removed.

Test plan

  • Scroll a busy x.com timeline for a while with devtools open — no console.error from attachReactTreeToMountedRoot, no duplicate Mask icons on posts whose author re-parses, DOM node / listener count stays flat.
  • Hover profile bio cards on x.com repeatedly — Mask badge still appears once, no console spam.
  • Facebook / Instagram / Minds feeds — avatar injection still works, no shadow-root pile-up.

🤖 Generated with Claude Code

https://claude.ai/code/session_013SLkuieqJXCMg1xoL98jij

guanbinrui and others added 2 commits September 5, 2026 23:37
Follow-up to #12432, which fixed the same class of leak for injectAvatar
and the tips buttons on x.com. The remaining injection points that create
a React tree inside an onNodeMutation/onTargetChanged handler had the same
problem: a mutation of the watched element (lazy image src swaps, hover
states, feed virtualization reusing nodes) re-runs the handler, which
attaches a brand-new shadow-root sibling + React tree without tearing down
the previous one. On a long scroll session this accumulates orphaned DOM
nodes / shadow roots / React roots (each with its own data-fetching hooks)
that are never cleaned up until navigation.

- facebook/minds/instagram injectAvatar: call remove() before re-creating
  the DOMProxy, mirroring the twitter.com fix from #12432.

- injectMaskIconToPostTwitter: post.author refines several times as post
  parsing completes and re-fires on every onNodeMutation of the tweet, so
  each add() stacked another DOMProxy shadow sibling + <Icon> React root
  (visible as duplicated Mask icons on the post). Reuse a single proxy,
  tear down the previous tree first, and release the post.author
  subscription on signal abort instead of leaking it.

- injectMaskUserBadgeAtTwitter (MaskIcon `_` helper): check() re-ran on
  every mutation and re-called attachReactTreeWithContainer with the same
  shadow key, which the mounter rejects with a console.error on every
  floating-bio-card hover while also clobbering `remover` to a no-op.
  Attach once; remove()/onRemove reset the guard so a genuinely re-added
  element still re-attaches.

- site-adaptor-infra startPostListener: the per-post AbortController wired
  a listener onto the page-lifetime signal on every post `set` and never
  removed it, so every post scrolled past left a permanent listener (and
  its retained controller). Pass { signal: abort.signal } so it drops when
  the post unmounts.

Output values are unchanged; only the redundant/leaked work is removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SLkuieqJXCMg1xoL98jij
@guanbinrui
guanbinrui merged commit 03c3409 into develop Sep 5, 2026
11 checks passed
@guanbinrui
guanbinrui deleted the perf/injection-react-tree-leaks branch September 5, 2026 16:03
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.

1 participant