PLG OS

Implementation worksheet · 5 min read

An In-App Component Teardown Checklist for SPA Frameworks

On every teardown path — completed, dismissed, navigated away, component unmounted, error — release eight things: event listeners, resize and mutation observers, timers and intervals, the focus trap and the previously focused element, any scroll lock, the portal node, the body class or inline style you added, and the subscription to application state. Test teardown by repeating the guidance ten times in one session and asserting that listener and observer counts return to baseline. The failures here do not appear on first use; they appear after repetition, which is why they reach production.

Overlay components take over more of the page than their size suggests: they trap focus, lock scrolling, listen for escape and resize, observe their target, and often add a class to the body. Each of those is a thing to undo, and frameworks only clean up what you told them about. What is missed does not error — it accumulates, and the symptoms show up somewhere else entirely, usually as sluggishness nobody connects to the tour.

Put it into practice

1. Enumerate every teardown path, not just the happy one

Completed, dismissed by the user, dismissed by escape, navigated away mid-step, unmounted by a parent re-render, and torn down after an error. Six paths. Most code cleans up on the first two. The remaining four are where leaks live, because they are the paths nobody demos.

2. Return focus to where it came from

Store the element that had focus before the overlay opened and restore it on teardown. Skipping this leaves keyboard and screen reader users at the top of the document with no context — it is an accessibility defect rather than a tidiness issue, and it is invisible to mouse users including whoever wrote it.

3. Release the scroll lock on every path

A scroll lock left on after an error-path teardown means the page cannot scroll and nothing indicates why. Users reload. This is the single most user-visible teardown failure and it comes entirely from the error path being untested.

4. Disconnect observers explicitly

Resize and mutation observers attached to a target are not garbage collected while the observer holds a reference. Disconnect them by name in teardown rather than trusting scope. A retained mutation observer on a busy DOM is real measurable cost, not a theoretical one.

5. Remove the portal node and any body styling

The container, the class you added, the inline overflow style. Each is cheap to leave and cumulative across a session. Ten dismissed tours leaving ten empty portal divs is a symptom that makes any later DOM debugging harder than it needs to be.

6. Test with repetition, not a single run

Run the guidance ten times in one page session and assert that listener counts, observer counts and portal nodes return to baseline. A single-run test passes with every one of these leaks present, which is precisely why single-run tests let them through.

Teardown checklist

Copy this structure into your review document and record your observed result for each row.

Teardown checklist
ResourceReleased on all pathsHow verifiedDone
Event listeners (keydown, click, resize)count at baseline after 10 runs
Resize observerexplicit disconnect
Mutation observerexplicit disconnect
Timers and intervalscleared by handle
Focus trap releasedfocus returns to opener
Scroll lock releasedpage scrolls after error path
Portal node removednode count at baseline
Body class or inline styleattribute check
State subscriptionunsubscribe called

A failure worth checking

The leak that only appears after eleven navigations. Each tour leaves one resize observer and one keydown listener attached. Nothing is noticeable at first. Somewhere past ten the application starts feeling sluggish on scroll, and because the guidance was dismissed long before, nobody connects the two. It gets investigated as a rendering performance problem for a week. Repetition testing turns this from an investigation into an assertion.

Common questions

Does the framework not clean this up?

It cleans up what it owns — the component tree, and effects you registered a cleanup for. Listeners on document, observers on elements outside the tree, body classes and portal nodes are outside that ownership. They are exactly the things overlays use most.

How do I count listeners in a test?

Wrap addEventListener and removeEventListener in the test environment and keep a tally, or use the browser's own inspection API through your end-to-end runner. Either way the assertion is the same: back to baseline after repeated runs.

Basis and scope

This is a proposed implementation method using illustrative examples, not a measured benchmark or a customer case study. Prepared with AI assistance. Validate product-specific behavior against current documentation and your own test environment.

Continue with PLG OS

Explore onboarding →