In 2026, why does a React admin app run fine in the morning but lag after opening a few detail pages in the afternoon, then recover after a browser restart—are listeners and timers not being cleaned up?
The bottom line first: In 2026, when a React page gets slower after being entered and exited several times and a browser restart makes it normal again, it is usually not routing or the framework slowing down. More often, several types of resources outside the component are not reclaimed on unmount: event listeners on window and document, timers and animation frames, IntersectionObserver and ResizeObserver, WebSocket and polling subscriptions, and third-party chart or map instances. React only removes the component from the tree; it will not call removeEventListener or clearInterval for you. Closures in callbacks also keep old state and DOM alive, so each enter/exit adds another layer.
So the entry point for troubleshooting is not to try a virtual list first, but to count what remains globally after each enter/exit.
Why lag keeps accumulating after the component unmounts
When React unmounts a component, it breaks React's internal references. Listeners attached to window are still there, timers are still running, and closures in callbacks keep the original data alive, so that memory cannot be reclaimed. A single leak is usually imperceptible. The real cost multiplier is high-frequency paths in admin and management apps, such as going from a list to a detail page and back: one person may enter and exit dozens of times a day, and the cost adds up linearly. The symptom is often not an error but rough scrolling, input delay, and dropped frames when switching pages. This is also why many people suspect large data volumes first, only to find that listener counts are rising.
- window/document events: resize, scroll, keydown, visibilitychange, postMessage
- Timers and animations: setInterval polling, recursive setTimeout, requestAnimationFrame loops
- Observers: IntersectionObserver, ResizeObserver, MutationObserver
- Connections and instances: WebSocket, EventSource, third-party charts / maps / players
Making troubleshooting concrete: inventory first, then use numbers
Group by resource type, not by page, because these four categories differ in detach method, failure mode, and verification tooling; listing by page easily misses items, while listing by type lets you check item by item.
- Inventory: search component files for addEventListener, setInterval, new Observer, new WebSocket, and new third-party instances, and write a checklist of what each useEffect attaches externally.
- Pair: every attach must have a detach, and attach and detach must use the same function reference; anonymous functions cannot be detached.
- Repeated enter/exit verification: enter and exit the same route 5 to 10 times (experience range), and observe whether JS heap memory and DOM node count rise in steps.
- Set the baseline: after returning to the list page, heap memory roughly falls back to around the pre-entry level plus 10% (experience range), and the count from getEventListeners(window) does not increase with the number of enter/exit cycles.
Step 2 is the most likely to cause rework: the code does include a return, but the detach is passed another newly written arrow function. Browsers only match references, so it is effectively not detached. Step 3 is often skipped, yet it is the only action that proves the fix is correct. It is worth adding it directly to the self-test checklist.
Several common traps are mostly the same type of mistake
- Passing a new function when detaching: addEventListener('resize', arrow function) is used on mount, and another one is written on unmount; the two are not the same reference.
- Putting objects in the dependency array: the dependencies contain objects or inline functions recreated on every render, so attach and detach alternate repeatedly, and duplicate requests are easily included.
- Suppressing warnings without fixing the root cause: a mounted flag is used to quiet setState warnings while listeners and timers remain.
- Mistaking StrictMode for a leak: in development, mounting runs twice; seeing a log printed one extra time makes people think there is a leak, but production builds do not behave this way.
- Only clearing DOM for third-party instances: the container content is cleared, but destroy or dispose is not called, so internal listeners and timers keep running.
The common thread is that they 'look handled'. To judge whether something is truly handled, do not look at whether return is written; look at the numbers from enter/exit verification.
Manual cleanup, custom hooks, or library-provided APIs: trade-offs
- Manual cleanup: return directly in each useEffect; per page, typical change effort is half an hour to two hours (experience range); it suits projects with few pages and a single resource type, but newcomers can easily miss it.
- Custom hooks: for example useEventListener and useInterval; the one-time cost is roughly 1 to 2 person-days (experience range); it suits multi-person collaboration and mid- and back-office systems with a dozen or more similar pages, but too thick an abstraction lengthens the debugging stack.
- Library-provided subscription APIs: for charts, maps, and players, prefer the official destroy; it suits pages with heavy third-party dependencies, at the cost of rechecking unmount APIs during version upgrades.
The boundary is clear: if a page has only one or two spots and will not change for a year, manual cleanup is enough; if similar resources recur, custom hooks pay off clearly; for any third-party instance, prefer official unmount interfaces rather than guessing.
Delivery scene: a typical rework
A common constraint looks like this: the admin module has a fixed budget, a two-week cycle, and assets and APIs delivered in parallel, so frontend can only ensure functional self-testing passes first. At the time, passing meant pages could enter and exit normally and the console showed no errors; nobody watched memory curves across repeated entries and exits. A week after launch, support reported that a data list got laggier as it scrolled, and it took two days to locate that the detail page's ResizeObserver and chart instance were not destroyed. The cost was about 1 to 2 person-days of extra rework (experience range), plus a delayed minor release. Now such pages add one item to the acceptance checklist: enter and exit the same route 10 times; heap memory does not rise in steps, and listener count does not increase with the number of cycles.
Applicable scenarios and boundaries
Priorities are: SPA admin and management pages, pages where users stay for long sessions, pages containing charts, maps, or players, and data pages with polling or WebSocket. Their common trait is that the same component is repeatedly mounted.
Also be clear about cases that do not need a major overhaul: a site with only one or two pages and components that do not unmount after mounting; pure static display pages; one-off campaign landing pages. Rewriting every useEffect usually does not justify the change risk.The criterion is whether the same component will be repeatedly mounted: if yes, paired cleanup is mandatory; if no, deliver functionality first.
FAQ
In React development, useEffect runs twice—is the cleanup function wrong?
No. StrictMode intentionally mounts, unmounts, and mounts again in development to expose code that lacks cleanup; production builds do not repeat like this.
I wrote return to detach, but the listener count still grows—where is the usual problem?
Most likely the attach and detach are not the same function reference, with separate arrow functions written on each side. Or the dependency array contains objects recreated on every render, causing attach and detach to alternate repeatedly.
If I do not change routes and only open and close a modal on the same page, can it still leak?
Yes. Conditional rendering, repeatedly opening and closing modals, and changing a component key to force a rebuild all trigger unmount; listeners and timers outside the component still need cleanup.
For chart or map components, is clearing the container enough on unmount?
No. Clearing the container only removes DOM; the instance's internal listeners, timers, and drawing context remain. Usually you need to call the official destroy or dispose.
Next time you see smooth mornings and laggy afternoons, check against the resource inventory first and let repeated enter/exit numbers speak; do not rush to virtual lists or a framework switch. This applies to components that are repeatedly mounted; for static pages mounted once, deliver functionality as usual.
-
Why does clicking price sort in a table put 10 before 2 in 2026? Is sort comparing strings by default?
Date: Oct 31, 2026 Read: 9
-
In 2026, clicking “Get verification code” in a modal submits the whole form too—is the button’s default type the culprit?
Date: Oct 30, 2026 Read: 17
-
In 2026, AI can ship a homepage fast, but images break and buttons stop responding on target devices—should frontend still own compatibility and performance?
Date: Oct 29, 2026 Read: 35
-
It's 2026—when my performance review asks what separates a frontend experience architect from a regular frontend developer, what evidence actually counts?
Date: Oct 28, 2026 Read: 26
-
It's 2026—why does a campaign page countdown fall behind after a few minutes on the lock screen, and why do users send screenshots asking if we cut their time?
Date: Oct 27, 2026 Read: 29




