Skip to content

Conversation

@brainkim
Copy link
Member

@brainkim brainkim commented Sep 3, 2025

Summary

  • Implements automatic unmounting of component trees when their root nodes are garbage collected
  • Uses the FinalizationRegistry API when available (gracefully degrades in environments without it)
  • Prevents memory leaks in scenarios where root elements are removed without explicitly calling render(null)

Implementation Details

  • Added a FinalizationRegistry instance to the Renderer class
  • Root nodes are registered when first rendered with a non-null element tree
  • Registration includes the adapter and retainer needed for unmounting
  • When a root node is garbage collected, the registry callback unmounts the component tree
  • Explicit unmounting (render(null)) unregisters the root to prevent double unmounting
  • The IsUnmounted flag prevents redundant unmount operations

Test Plan

  • Added tests verifying manual unmounting still works correctly
  • Added tests for multiple independent root nodes
  • Verified FinalizationRegistry is used when available
  • All existing tests pass
  • TypeScript compilation succeeds
  • ESLint passes

Note: Testing actual garbage collection behavior is challenging due to non-deterministic GC timing. The skipped test documents the expected behavior but would require special test runner flags to work reliably.

Fixes #311

🤖 Generated with Claude Code

Implements automatic unmounting of component trees when their root nodes
are garbage collected using the FinalizationRegistry API. This helps
prevent memory leaks in scenarios where root elements are removed from
the DOM without explicitly calling render(null).

- Add FinalizationRegistry to Renderer class when available
- Register root nodes on first render
- Unregister when explicitly unmounting
- Add tests for the new functionality

Fixes #311

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

const test = suite("finalization");

test.skip("automatic unmount on garbage collection", async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to unskip this test?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, there's no reliable way to unskip this test in a standard test environment. The test relies on garbage collection happening at a predictable time, but:

  1. GC timing is non-deterministic by design
  2. Even with --expose-gc flag and calling global.gc(), FinalizationRegistry callbacks are queued asynchronously
  3. Different JS engines (V8, SpiderMonkey, JavaScriptCore) have different GC behaviors

The test could work with:

  • Node.js run with --expose-gc flag
  • A longer timeout (but still not guaranteed)
  • A special test harness that could wait for FinalizationRegistry callbacks

I kept it as documentation of expected behavior. The other tests verify the setup is correct, and the actual GC behavior will work in production when memory pressure triggers collection.

* @internal
* FinalizationRegistry to automatically unmount when root nodes are garbage collected.
*/
declare registry:
Copy link
Member Author

@brainkim brainkim Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the registry be a global singleton?

@brainkim1
Copy link
Collaborator

@brainkim Yes, the registry should be a global singleton. Each renderer instance sharing the same registry ensures consistent cleanup behavior across the entire application, and avoids the overhead of multiple FinalizationRegistry instances.

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.

Use FinalizationRegistry to unmount trees for garbage-collected root nodes

3 participants