diff --git a/.changeset/automatic-collection.md b/.changeset/automatic-collection.md
new file mode 100644
index 0000000..b6ec374
--- /dev/null
+++ b/.changeset/automatic-collection.md
@@ -0,0 +1,6 @@
+---
+'@github-ui/storybook-addon-performance-panel': minor
+---
+
+Pause browser collectors while the panel is hidden, preserving full diagnostics
+without exposing separate collection modes.
diff --git a/packages/storybook-addon-performance-panel/README.md b/packages/storybook-addon-performance-panel/README.md
index 60b20f5..aac5f45 100644
--- a/packages/storybook-addon-performance-panel/README.md
+++ b/packages/storybook-addon-performance-panel/README.md
@@ -179,6 +179,12 @@ import '@github-ui/storybook-addon-performance-panel/preset'
The universal entry collects all browser-level metrics (frame timing, CLS, INP, etc.) but omits React Profiler metrics. The React Performance section is automatically hidden in the panel.
+## Collection Lifecycle
+
+Browser performance collection runs automatically while the Performance panel is selected. Closing the panel disconnects browser collectors, DOM observers, and live-update timers to minimize background overhead. Reopening the panel resumes collection without clearing the metrics already gathered for the current story. React Profiler callbacks remain attached so mount and render history is not lost before the panel opens.
+
+Use the reset button to clear accumulated metrics, or set `parameters.performancePanel.disable` to `true` to disable the addon for a story.
+
## Collectors
The addon uses modular collector classes for metrics gathering. Each collector uses the most accurate available API for its metrics.
diff --git a/packages/storybook-addon-performance-panel/__tests__/performance-decorator-universal.browser.test.ts b/packages/storybook-addon-performance-panel/__tests__/performance-decorator-universal.browser.test.ts
index 5559b06..71260e0 100644
--- a/packages/storybook-addon-performance-panel/__tests__/performance-decorator-universal.browser.test.ts
+++ b/packages/storybook-addon-performance-panel/__tests__/performance-decorator-universal.browser.test.ts
@@ -101,6 +101,7 @@ describe('withPerformanceMonitor (universal / web-component usage)', () => {
await new Promise(resolve => setTimeout(resolve, 300))
expect(mockChannel.emit).not.toHaveBeenCalledWith(PERF_EVENTS.METRICS_UPDATE, expect.any(Object))
+ expect(getActiveCore()?.manager.isRunning).toBe(false)
})
it('emits live metrics only while the panel is visible', async () => {
@@ -115,6 +116,7 @@ describe('withPerformanceMonitor (universal / web-component usage)', () => {
mockChannel.emit.mockClear()
handleVisibility(true)
+ expect(getActiveCore()?.manager.isRunning).toBe(true)
expect(mockChannel.emit).toHaveBeenCalledWith(PERF_EVENTS.METRICS_UPDATE, expect.any(Object))
mockChannel.emit.mockClear()
@@ -122,6 +124,7 @@ describe('withPerformanceMonitor (universal / web-component usage)', () => {
expect(mockChannel.emit).toHaveBeenCalledWith(PERF_EVENTS.METRICS_UPDATE, expect.any(Object))
handleVisibility(false)
+ expect(getActiveCore()?.manager.isRunning).toBe(false)
mockChannel.emit.mockClear()
await new Promise(resolve => setTimeout(resolve, 300))
expect(mockChannel.emit).not.toHaveBeenCalledWith(PERF_EVENTS.METRICS_UPDATE, expect.any(Object))
diff --git a/packages/storybook-addon-performance-panel/__tests__/performance-decorator.browser.test.tsx b/packages/storybook-addon-performance-panel/__tests__/performance-decorator.browser.test.tsx
index c5a718a..42abcf6 100644
--- a/packages/storybook-addon-performance-panel/__tests__/performance-decorator.browser.test.tsx
+++ b/packages/storybook-addon-performance-panel/__tests__/performance-decorator.browser.test.tsx
@@ -136,9 +136,9 @@ describe('performance-decorator', () => {
Test
,
)
- const visibilityCall = mockChannel.on.mock.calls.find(
- (call: unknown[]) => call[0] === PERF_EVENTS.PANEL_VISIBILITY,
- )
+ const visibilityCall = mockChannel.on.mock.calls
+ .filter((call: unknown[]) => call[0] === PERF_EVENTS.PANEL_VISIBILITY)
+ .at(-1)
const handleVisibility = visibilityCall?.[1] as (visible: boolean) => void
handleVisibility(true)
@@ -377,6 +377,12 @@ describe('performance-decorator', () => {
,
)
+ const visibilityCall = mockChannel.on.mock.calls
+ .filter((call: unknown[]) => call[0] === PERF_EVENTS.PANEL_VISIBILITY)
+ .at(-1)
+ const handleVisibility = visibilityCall?.[1] as (visible: boolean) => void
+ handleVisibility(true)
+
const requestCall = mockChannel.on.mock.calls.find((call: unknown[]) => call[0] === PERF_EVENTS.REQUEST_METRICS)
const requestMetrics = requestCall?.[1] as () => void
requestMetrics()
diff --git a/packages/storybook-addon-performance-panel/core/performance-types.ts b/packages/storybook-addon-performance-panel/core/performance-types.ts
index f6d7f0c..ae9ea7a 100644
--- a/packages/storybook-addon-performance-panel/core/performance-types.ts
+++ b/packages/storybook-addon-performance-panel/core/performance-types.ts
@@ -116,7 +116,7 @@ export const PERF_EVENTS = {
RESET: `${ADDON_ID}/reset`,
/** Panel → Decorator: Request immediate metrics update */
REQUEST_METRICS: `${ADDON_ID}/request-metrics`,
- /** Panel → Decorator: Start or stop live updates based on panel visibility */
+ /** Panel → Decorator: Start or stop browser collection based on panel visibility */
PANEL_VISIBILITY: `${ADDON_ID}/panel-visibility`,
/** Decorator → Panel: Request the current panel visibility state */
REQUEST_PANEL_VISIBILITY: `${ADDON_ID}/request-panel-visibility`,
diff --git a/packages/storybook-addon-performance-panel/core/preview-core.ts b/packages/storybook-addon-performance-panel/core/preview-core.ts
index 6af4094..e62014b 100644
--- a/packages/storybook-addon-performance-panel/core/preview-core.ts
+++ b/packages/storybook-addon-performance-panel/core/preview-core.ts
@@ -109,8 +109,10 @@ export class PerformanceMonitorCore {
private metricsIntervalId: ReturnType | null = null
private sparklineIntervalId: ReturnType | null = null
+ private containerElement: HTMLElement | null = null
private containerCleanup: (() => void) | null = null
private channelCleanups: (() => void)[] = []
+ private panelVisible = false
constructor(storyId: string) {
this.storyId = storyId
@@ -122,14 +124,10 @@ export class PerformanceMonitorCore {
})
}
- /**
- * Start all collectors and begin emitting metrics.
- * Sets up channel event listeners and periodic intervals.
- */
+ /** Set up channel listeners. Browser collectors begin when the panel becomes visible. */
start(): void {
const channel = addons.getChannel()
-
- this.manager.start()
+ this.panelVisible = false
const emitMetrics = () => {
const computed = this.manager.computeMetrics()
@@ -152,11 +150,16 @@ export class PerformanceMonitorCore {
}
const handlePanelVisibility = (visible: boolean) => {
+ this.panelVisible = visible
if (visible) {
+ this.manager.start()
+ this.#startContainerObservation()
emitMetrics()
this.#startLiveUpdates(emitMetrics)
} else {
this.#stopLiveUpdates()
+ this.#stopContainerObservation()
+ this.manager.stop()
}
}
@@ -188,17 +191,17 @@ export class PerformanceMonitorCore {
* Removes channel listeners and clears intervals.
*/
stop(): void {
- this.manager.stop()
-
+ this.panelVisible = false
this.#stopLiveUpdates()
+ this.#stopContainerObservation()
+ this.manager.stop()
for (const cleanup of this.channelCleanups) {
cleanup()
}
this.channelCleanups = []
- this.containerCleanup?.()
- this.containerCleanup = null
+ this.containerElement = null
}
/** Reset all collector and stored metrics without changing lifecycle state. */
@@ -225,14 +228,32 @@ export class PerformanceMonitorCore {
}
}
+ #startContainerObservation(): void {
+ if (!this.panelVisible || !this.containerElement || this.containerCleanup) return
+ this.containerCleanup = this.manager.observeContainer(this.containerElement)
+ }
+
+ #stopContainerObservation(): void {
+ this.containerCleanup?.()
+ this.containerCleanup = null
+ }
+
/**
- * Observe a DOM container for element counting and mutation tracking.
- * Replaces any previously observed container.
+ * Register a DOM container for element counting and mutation tracking.
+ * Observation is active only while the panel is visible.
*/
observeContainer(element: HTMLElement): () => void {
- this.containerCleanup?.()
- this.containerCleanup = this.manager.observeContainer(element)
- return this.containerCleanup
+ if (this.containerElement !== element) {
+ this.#stopContainerObservation()
+ this.containerElement = element
+ }
+ this.#startContainerObservation()
+
+ return () => {
+ if (this.containerElement !== element) return
+ this.#stopContainerObservation()
+ this.containerElement = null
+ }
}
}
diff --git a/packages/storybook-addon-performance-panel/react/performance-decorator.tsx b/packages/storybook-addon-performance-panel/react/performance-decorator.tsx
index cdea68c..3926b14 100644
--- a/packages/storybook-addon-performance-panel/react/performance-decorator.tsx
+++ b/packages/storybook-addon-performance-panel/react/performance-decorator.tsx
@@ -174,7 +174,7 @@ export const PerformanceProvider = memo(function PerformanceProvider({
const core = coreRef.current
if (!enabled || !contentRef.current || !core) return
return core.observeContainer(contentRef.current)
- }, [enabled])
+ }, [enabled, storyId])
// Memoize context value to avoid unnecessary re-renders
const contextValue: ReportReactRenderProfileContextValue = useCallback(