Memory hygiene guide
Every timer, and where it stops
An animation library with a leak is worse than a slow one: a page that keeps a timer alive keeps the CPU awake and the battery draining long after the tab is hidden. This audit counts registrations against cleanups across the whole source tree, which is a code reading rather than a profiler trace — the honest limits of which are stated below the table.
Intervals / timeouts
110 set · 76 cleared
setInterval + setTimeout against clearInterval + clearTimeout
Animation frames
70 requested · 52 cancelled
a one-shot frame never needs cancelling
Listeners
57 added · 51 removed
element and window listeners
Registrations against cleanups, per file
A file where registrations outnumber cleanups is not automatically wrong — a one-shot requestAnimationFrame and a listener on an element that is going away both need no cleanup. It is a list to read, not a verdict, and the files with the biggest gaps are at the top.
| File | setInterval/Timeout | clear… | rAF | cancelAF | addListener | removeListener |
|---|---|---|---|---|---|---|
| asset-detail.tsx | 15 | 11 | 3 | 1 | 2 | 1 |
| demos/scenes/set-04.tsx | 12 | 5 | 0 | 0 | 1 | 1 |
| demos/scenes-17.tsx | 2 | 2 | 9 | 3 | 2 | 2 |
| community-ui-2.tsx | 0 | 0 | 5 | 5 | 7 | 7 |
| community-ui.tsx | 1 | 1 | 6 | 6 | 4 | 4 |
| admin-ui-2.tsx | 0 | 0 | 4 | 4 | 6 | 6 |
| retention-ui-2.tsx | 0 | 0 | 3 | 3 | 7 | 7 |
| demos/scenes/set-01.tsx | 2 | 3 | 5 | 4 | 1 | 1 |
| demos/scenes/set-02.tsx | 7 | 6 | 0 | 0 | 0 | 0 |
| demos/scenes/set-11.tsx | 6 | 8 | 0 | 0 | 1 | 1 |
| demos/scenes/set-05.tsx | 4 | 3 | 2 | 1 | 1 | 1 |
| lab-tools.tsx | 2 | 0 | 5 | 3 | 0 | 0 |
The house rules the code follows, and the ones this table is read against: every setInterval is cleared in the effect cleanup; every addEventListener is paired with a remove in the same effect; a loop that needs to run while visible is driven by one frame at a time rather than by a timer at a fixed rate.
29 of 63 files register more timers or frames than they clear. The largest gap is asset-detail.tsx — worth a read rather than a rewrite, since a one-shot frame accounts for most gaps of one.
Loops in scenes
74
20 interval · 36 timeout · 18 rAF
Intervals cleared
20/20
setInterval against clearInterval
Sub-100 ms intervals
6
tight loops, flagged below
Uncleaned intervals
0
a loop with no clear in its scene
Every loop in the scene modules, by scene
Extracted from source at build time: the scene, the kind of loop, its interval and whether the same function cleans it up. This is a registry, not a verdict — a one-shot rAF and a timeout on a message that goes away both legitimately need no cleanup — but a setInterval with no clear, or one firing faster than 100 ms, is visible here before a browser has to prove it leaks.
| Scene | Kind | Interval | Cleanup | Where |
|---|---|---|---|---|
| ReorderList | raf | () => { el.style.transition = reduced ? "none" : "transform 220ms cubic-bezier(0.22, 1, 0.36, 1)"; el.style.transform = ""; } | one-shot | scenes-17.tsx:87 |
| DrawOnScroll | raf | () => setDrawn(true) | one-shot | scenes-17.tsx:1113 |
| MorphIcons | raf | tick | cleared | scenes-17.tsx:1222 |
| MorphIcons | raf | tick | cleared | scenes-17.tsx:1224 |
| AuditRing | raf | step | cleared | scenes-17.tsx:1520 |
| AuditRing | raf | step | cleared | scenes-17.tsx:1532 |
| AuditRing | raf | step | cleared | scenes-17.tsx:1534 |
| OdometerValue | raf | step | cleared | scenes-17.tsx:1900 |
| OdometerValue | raf | step | cleared | scenes-17.tsx:1902 |
| PulseGraph | interval | 900 | cleared | scenes-17.tsx:2766 |
| PreloaderHandoff | interval | STAGE_MS | cleared | scenes-17.tsx:3040 |
| HaloTrail | raf | () => { setHalos((prev) => prev.filter((h) => h.id > id - n)); } | cleared | scenes/set-01.tsx:204 |
| ScrambleText | raf | tick | cleared | scenes/set-01.tsx:400 |
| ScrambleText | raf | tick | cleared | scenes/set-01.tsx:402 |
| CounterStats | raf | tick | cleared | scenes/set-01.tsx:906 |
| CounterStats | raf | tick | cleared | scenes/set-01.tsx:908 |
| DotDraw | timeout | 900 | cleared | scenes/set-01.tsx:962 |
| TextCycle | interval | 2600 | cleared | scenes/set-01.tsx:1030 |
| CommandPalette | timeout | 1400 | uncleared | scenes/set-02.tsx:191 |
| ToastStack | timeout | ttl | uncleared | scenes/set-02.tsx:285 |
| TestimonialRotator | interval | sp | cleared | scenes/set-02.tsx:630 |
| CountdownDrop | interval | 1000 | cleared | scenes/set-02.tsx:693 |
| TerminalHero | timeout | sp | cleared | scenes/set-02.tsx:759 |
| TerminalHero | timeout | 4200 | cleared | scenes/set-02.tsx:764 |
| OdometerCounter | interval | 42 | cleared | scenes/set-02.tsx:1027 |
| QuantityStepper | interval | 110 | cleared | scenes/set-03.tsx:188 |
| FileDropZone | interval | 90 | cleared | scenes/set-03.tsx:426 |
| FileDropZone | timeout | 250 | uncleared | scenes/set-03.tsx:431 |
| SplitButtonMenu | timeout | 700 | uncleared | scenes/set-03.tsx:640 |
| SkeletonCardInner | timeout | delay | cleared | scenes/set-04.tsx:176 |
| StatusBanner | timeout | 1600 | uncleared | scenes/set-04.tsx:264 |
| ProgressRing | interval | 130 | cleared | scenes/set-04.tsx:285 |
| ProgressRing | timeout | 200 | uncleared | scenes/set-04.tsx:289 |
| SpinnerStatus | timeout | 1400 | uncleared | scenes/set-04.tsx:385 |
| SpinnerStatus | timeout | 3200 | uncleared | scenes/set-04.tsx:386 |
| OfflineIndicator | timeout | 2800 | uncleared | scenes/set-04.tsx:490 |
| OfflineIndicator | timeout | 3400 | uncleared | scenes/set-04.tsx:491 |
| ErrorBoundaryCard | timeout | 1400 | uncleared | scenes/set-04.tsx:541 |
| DotLeaderLoading | timeout | 2500 | cleared | scenes/set-04.tsx:647 |
| StaggeredListEntrance | timeout | 60 | uncleared | scenes/set-04.tsx:1034 |
| StaggeredListEntrance | timeout | 620 | uncleared | scenes/set-04.tsx:1035 |
| ShuffleKenburnsGallery | timeout | 6200 | cleared | scenes/set-05.tsx:53 |
| ParticleTrailHero | timeout | 850 | cleared | scenes/set-05.tsx:180 |
| RippleReveal | timeout | 800 | uncleared | scenes/set-05.tsx:395 |
| WordHighlight | timeout | ms | cleared | scenes/set-05.tsx:583 |
| StatsBand | raf | tick | cleared | scenes/set-05.tsx:1063 |
| StatsBand | raf | tick | cleared | scenes/set-05.tsx:1065 |
| MapFreeLocalBand | interval | 30000 | cleared | scenes/set-06.tsx:636 |
| AppScreenshotTour | timeout | 3400 | cleared | scenes/set-06.tsx:713 |
| TemplateWaitlist | interval | 1000 | cleared | scenes/set-06.tsx:994 |
| TemplateWaitlist | timeout | 1400 | uncleared | scenes/set-06.tsx:1009 |
| TemplateGallery | timeout | 1200 | uncleared | scenes/set-07.tsx:26 |
| WaterRipple | timeout | 2200 | cleared | scenes/set-07.tsx:652 |
| RippleButton | timeout | 600 | uncleared | scenes/set-08.tsx:29 |
| NumberTickerScroll | raf | step | one-shot | scenes/set-08.tsx:69 |
| NumberTickerScroll | raf | step | one-shot | scenes/set-08.tsx:71 |
| LongPressButton | interval | 40 | cleared | scenes/set-08.tsx:94 |
| CopyIconButton | timeout | 1400 | uncleared | scenes/set-08.tsx:155 |
| ScrambleOnView | interval | 50 | cleared | scenes/set-11.tsx:30 |
| TypewriterCursor | interval | 60 | cleared | scenes/set-11.tsx:72 |
| TabAwareCounter | interval | 40 | cleared | scenes/set-11.tsx:93 |
| FlipClock | interval | 5000 | cleared | scenes/set-11.tsx:264 |
| PointerImageTrail | timeout | 800 | uncleared | scenes/set-11.tsx:343 |
| LetterRise | timeout | 200 | cleared | scenes/set-11.tsx:430 |
| PullToRefresh | timeout | 900 | uncleared | scenes/set-12.tsx:177 |
| ToastTimerBar | timeout | 4000 | uncleared | scenes/set-12.tsx:399 |
| VerticalCarousel | interval | 4000 | cleared | scenes/set-12.tsx:425 |
| HeadlineSwap | interval | 2600 | cleared | scenes/set-12.tsx:456 |
| TimeZonePicker | timeout | 0 | cleared | scenes/set-14.tsx:39 |
| TimeZonePicker | interval | 30000 | cleared | scenes/set-14.tsx:42 |
| CronEditor | timeout | 0 | cleared | scenes/set-14.tsx:157 |
| CronEditor | interval | 30000 | cleared | scenes/set-14.tsx:161 |
| ChatComposer | timeout | 0 | cleared | scenes/set-14.tsx:366 |
| ChatComposer | timeout | 1500 | cleared | scenes/set-14.tsx:388 |
What this audit does not do
It counts call sites, not live handles. A file with equal registrations and cleanups can still leak if the cleanup is in the wrong place, and a file with a gap of one is usually a single-shot frame that never needs cancelling. The measurement that settles it is a heap snapshot after navigating between twenty pages, and that needs a browser attached — which is on the device matrix page as a method, not as a result.
Per-frame work
#406 — a rAF loop is the only thing here that runs per frame, and the registry counts them: every requestAnimationFrame in the scene modules is a named row with its cleanup stated. The interval table above is the tick side; this is the frame side. The heaviest consumers are named rather than averaged.