Skip to content

perf(storybook): stop action spies from serializing DOM events - #1288

Merged
tenphi merged 2 commits into
mainfrom
fix-investigate-storybook-freeze
Aug 7, 2026
Merged

perf(storybook): stop action spies from serializing DOM events#1288
tenphi merged 2 commits into
mainfrom
fix-investigate-storybook-freeze

Conversation

@tenphi

@tenphi tenphi commented Aug 7, 2026

Copy link
Copy Markdown
Member

What

Storybook auto-wires an action() spy to every on* prop it finds in a component's argTypes (addActionsFromArgTypes). React Aria calls those spies with the raw React synthetic event, and the preview channel then serializes the payload with telejson at maxDepth: 15 — once for the postMessage transport, and again for the dev-server websocket, which hardcodes that depth and ignores per-event options.

A synthetic event reaches nativeEvent, target and _targetInst (the React fiber), so that walk drags in the entire component tree: ~174k JSON.stringify calls and ~435k regex tests per focus change, in every story.

.storybook/actionArgs.js adds an argsEnhancers entry that wraps each action arg so event-like arguments become a compact plain summary before the spy runs. Wrapping from the outside keeps it independent of both transports' depth settings.

before after
Forms/TextInput focus change ~1150ms ~1.0ms
Forms/ColorPicker focus change ~570ms ~1.0ms
JSON.stringify calls / 2 focus changes 683,787 56

Why

Moving focus between elements cost 570–1250ms of synchronous browser work on any dev Storybook page rendering a Cube component, making forms unusable to develop against.

The original suspect was the 629 @property rules the Glaze palette registers. That was ruled out directly:

  • Deleting all 629 registrations at runtime (verified gone — --white-color stopped resolving to its registered initial value) changed nothing: ~1100ms before, ~1100ms after.
  • Disabling every stylesheet one at a time, including the 728-rule tasty sheet, changed nothing.
  • During the slow window: 0 getComputedStyle, 0 getBoundingClientRect, 0 insertRule, 0 setAttribute, 0 DOM mutations.
  • Measuring the registrations head-on — a full-document recalc over 3021 nodes flipping inherited color tokens at :root — gives 2.0ms with the 629 rules and 2.0ms without.

Bracketing the event path put the time entirely between the bubble listener on #cube-ui-kit-root and the one on #storybook-root (React's root listener), and stack sampling landed in PostMessageTransport.send → telejson stringify.

Notes for reviewers

  • Dev-server-only. A production build has no channel and no action spies. Verified by building the library and a minimal Vite production consumer: focusing a TextInput costs <1.1ms with all 629 @property rules still registered in the page. No change to src/tokens/palette.ts is warranted.
  • The Actions panel still works, and reads better than before: { name: "blur", args: { __event: "blur", target: "input#«r0»[data-qa=\"Input\"]", value: "" } }.
  • The wrapped spies record the summarized args. No story in the repo asserts on action args (grepped for expect(args / fn() — zero hits), but a future play function asserting against a live event object would see the summary instead.
  • Adding a new file imported by preview.jsx is not hot-reloaded — a running pnpm storybook needs a restart to pick this up.
  • Empty changeset per CONTRIBUTING: .storybook/** is not part of the published package.

🤖 Generated with Claude Code


Note

Low Risk
Storybook dev tooling only; no runtime library changes, with the only behavioral nuance being summarized action args in the Actions panel.

Overview
Fixes severe Storybook dev lag on focus changes (hundreds of ms per interaction) caused by telejson walking full React synthetic events when auto-wired Actions spies fire.

Adds .storybook/actionArgs.js with an argsEnhancers hook that wraps each isAction handler so event-like and DOM node arguments are replaced with compact summaries (event type, target selector, key/value) before the spy runs and the preview channel serializes them. preview.jsx re-exports that enhancer.

Dev-only — no published package behavior change; empty changeset for .storybook/**. Actions panel still works; recorded args are summaries instead of live events (no stories assert on raw event objects today).

Reviewed by Cursor Bugbot for commit 98bb3d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Storybook auto-wires an `action()` spy to every `on*` prop it finds in a
component's argTypes. React Aria calls those spies with the raw React
synthetic event, and the preview channel then serializes the payload with
telejson at `maxDepth: 15` — once for the postMessage transport, and again
for the dev-server websocket, which hardcodes that depth and ignores
per-event options.

A synthetic event reaches `nativeEvent`, `target` and `_targetInst` (the
React fiber), so that walk drags in the whole component tree: ~174k
`JSON.stringify` calls and ~435k regex tests, i.e. ~600ms of synchronous
work on every focus change, in every story.

Wrap each action arg so event-like arguments become a compact plain summary
before the spy runs. Wrapping from the outside keeps this independent of
both transports' depth settings.

Forms/TextInput focus change: ~1150ms -> ~1.0ms.
Forms/ColorPicker focus change: ~570ms -> ~1.0ms.
`JSON.stringify` calls per two focus changes: 683787 -> 56.

Dev-server-only; a production build has no channel and no action spies.
Verified: focusing a TextInput in a Vite production bundle costs <1.1ms
with all 629 `@property` rules still registered in the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 98bb3d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cube-ui-kit Ready Ready Preview Aug 7, 2026 5:46pm

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-6a59f5a.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 456.91 KB (0% 🟰) Yes 🎉
Tree shaking (just a Button) 118.94 KB (0% 🟰) Yes 🎉

Click here if you want to find out what is changed in this build

@tenphi
tenphi merged commit 16b6771 into main Aug 7, 2026
15 checks passed
@tenphi
tenphi deleted the fix-investigate-storybook-freeze branch August 7, 2026 17:50
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.

1 participant