Skip to content

[Bug Report] useTheme: browser adapter leaks adopted stylesheet on dispose #399

Description

@johnleider

Summary

The useTheme browser adapter appends a CSSStyleSheet to document.adoptedStyleSheets but never removes it on dispose() — only Vue watchers are stopped. Repeated mount/unmount (HMR, test suites, micro-frontend / multi-app teardown) accumulates orphaned stylesheets.

Location

  • packages/0/src/composables/useTheme/adapters/v0.ts:122-130upsert() creates this.sheet and appends it to document.adoptedStyleSheets.
  • :64, :75, :89-92 — all three dispose assignments stop watchers only; none remove the sheet or clear this.sheet.
  • dispose is invoked on teardown via useTheme/index.ts:399 (app.onUnmount(() => adapter.dispose?.())).
  • Precedent: the sibling useTheme/adapters/unhead.ts:111-113 correctly disposes its injected style.

Impact

Single long-lived SPA: negligible (the sheet should live for the app lifetime). Repeated mount/unmount: orphaned stylesheets accumulate. Each sheet is tiny and the theme CSS is idempotent ([data-theme]-scoped vars), so this is memory/cleanliness growth, not visual or correctness breakage. SSR is unaffected (the branch is IN_BROWSER-gated). Low severity.

Suggested fix

In dispose, remove the sheet and clear the ref, across all three assignment sites:

document.adoptedStyleSheets = document.adoptedStyleSheets.filter(s => s !== this.sheet)
this.sheet = undefined

Follow-up to #342 (leak-safe adapter lifecycle), which added dispose → app.onUnmount for watchers but did not cover the adopted stylesheet the adapter owns. Optional: a regression test asserting the sheet is gone from document.adoptedStyleSheets after app.unmount().

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions