Performance audit finding · Severity: Medium (batch) · Effort: Medium · Fix risk: Simple · Test safety net: Partial
Owner: per-file CODEOWNERS
Scope: timers and listeners registered in effects without cleanup, app-wide — GitHub code search sizes the triage surface at 39 files co-locating setInterval with useEffect and 34 files co-locating addEventListener with useEffect under app/ (file-level co-occurrence; each hit needs a teardown check, many will be fine)
What is this about?
Every setInterval, setTimeout, and addEventListener inside a useEffect must be torn down in the effect's cleanup function. A leaked interval keeps firing (and dispatching) after unmount; a leaked listener pins its closure — and everything the closure captured — out of garbage collection. Mount/unmount cycles stack additional copies. The extension's equivalent sweep (MetaMask-planning#6541) found 122 instances; mobile's known anchors:
Detection recipe (from the skill's mm-useeffect-antipatterns / audit playbook):
grep -rn "setInterval\|setTimeout\|addEventListener\|\.on(" app --include="*.ts" --include="*.tsx" | grep -v ".test." | grep -vE "clear|remove|off\("
For each hit, confirm a matching teardown (return () => … with clearInterval/clearTimeout/removeEventListener/.off()); fix or waive with a stated reason (app-lifetime singletons).
Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Fix per site: return the teardown from the effect. Once patterns repeat, prefer the shared-hook route already prescribed in #31495 (useEventListener with automatic cleanup, per the extension's MetaMask-planning#7166) so cleanup is the default rather than per-site diligence.
Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
- All sweep hits triaged: teardown added or waived with reason.
- Navigate-in/navigate-out N times on touched screens: timer/listener counts stay flat (heap snapshot or
js-memory-leaks recipe); no post-unmount dispatches.
References
What is this about?
Every
setInterval,setTimeout, andaddEventListenerinside auseEffectmust be torn down in the effect's cleanup function. A leaked interval keeps firing (and dispatching) after unmount; a leaked listener pins its closure — and everything the closure captured — out of garbage collection. Mount/unmount cycles stack additional copies. The extension's equivalent sweep (MetaMask-planning#6541) found 122 instances; mobile's known anchors:app/core/SDKConnectV2/services/connection-registry.ts:487—AppStatelistener without cleanup (catalogued in themms-performanceskill)Detection recipe (from the skill's
mm-useeffect-antipatterns/ audit playbook):For each hit, confirm a matching teardown (
return () => …withclearInterval/clearTimeout/removeEventListener/.off()); fix or waive with a stated reason (app-lifetime singletons).Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Fix per site: return the teardown from the effect. Once patterns repeat, prefer the shared-hook route already prescribed in #31495 (
useEventListenerwith automatic cleanup, per the extension's MetaMask-planning#7166) so cleanup is the default rather than per-site diligence.Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
js-memory-leaksrecipe); no post-unmount dispatches.References
AbortController/ cancelled flag) to async effects thatsetStateafter resolve #31495 (async-effect cancellation; shares the shared-hooks fix direction)mms-performancesweep recipes (perf: Augmentmms-performancewith frontend learnings from the Extension performance audit skills#49,mm-useeffect-antipatterns)