Skip to content

Core: Avoid hanging when inferring args for recursive calls on DOM elemens#33922

Merged
valentinpalkovic merged 1 commit into
nextfrom
valentin/fix-performance-in-infer-arg-types
Feb 25, 2026
Merged

Core: Avoid hanging when inferring args for recursive calls on DOM elemens#33922
valentinpalkovic merged 1 commit into
nextfrom
valentin/fix-performance-in-infer-arg-types

Conversation

@valentinpalkovic
Copy link
Copy Markdown
Contributor

@valentinpalkovic valentinpalkovic commented Feb 25, 2026

Closes #33821
Closes #17098
Closes #19575
Closes #28750
Closes #17482
Closes #16855

What I did

The problem (thanks @sonsu-lee for the investigations!):

inferType() in inferArgTypes.ts recursively traversed all properties of any object to infer arg types for the Controls panel. When args contained refs (createRef()) whose .current pointed to DOM elements after rendering, this caused exponential traversal through the DOM tree and React Fiber internals, freezing the browser tab.

The existing visited Set uses new Set(visited) at each branch for sibling path independence, so the same object reached via different paths is not detected as a cycle — leading to exponential traversal on wide objects like DOM elements (200+ properties each).

Fix

Unlike in the original PR, which attempts to solve the issue by being very conservative about which data types to support, I have used a Map to cache the inferred results and share the same visited set across all branches, reducing theamount of warning messages in scenarios, where e.g. plain class instances may be allowed as args.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This pull request has been released as version 0.0.0-pr-33922-sha-8e1bafbc. Try it out in a new sandbox by running npx storybook@0.0.0-pr-33922-sha-8e1bafbc sandbox or in an existing project with npx storybook@0.0.0-pr-33922-sha-8e1bafbc upgrade.

More information
Published version 0.0.0-pr-33922-sha-8e1bafbc
Triggered by @valentinpalkovic
Repository storybookjs/storybook
Branch valentin/fix-performance-in-infer-arg-types
Commit 8e1bafbc
Datetime Wed Feb 25 09:40:40 UTC 2026 (1772012440)
Workflow run 22391094558

To request a new release of this pull request, mention the @storybookjs/core team.

core team members can create a new canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=33922

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling and detection of circular references in component argument types.
  • Performance

    • Argument type inference performance enhanced through caching mechanism to reduce redundant computations.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 25, 2026

Fails
🚫 The "#### Manual testing" section must be filled in. Please describe how to test the changes you've made, step by step, so that reviewers can confirm your PR works as intended.

Generated by 🚫 dangerJS against 8e1bafb

@valentinpalkovic valentinpalkovic self-assigned this Feb 25, 2026
@valentinpalkovic valentinpalkovic changed the title Core: Avoid performance bottlenecks when infering args for recursive calls on DOM elemens Core: Avoid hanging when inferring args for recursive calls on DOM elemens Feb 25, 2026
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Feb 25, 2026

View your CI Pipeline Execution ↗ for commit 8e1bafb

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ❌ Failed 5m 7s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-25 11:06:07 UTC

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Adds caching and cycle-aware traversal to type inference in inferArgTypes.ts. Introduces a cache Map for memoizing computed SBType results and a visited set for detecting cyclic references. Logs warnings when cycles are encountered and cleans up traversal state after successful processing.

Changes

Cohort / File(s) Summary
Type Inference Caching
code/core/src/preview-api/modules/store/inferArgTypes.ts
Adds memoization cache and cycle detection to type inference. Expands internal inferType signature to accept cache and visited set parameters, implements cache lookups and writes for non-primitive values, introduces cycle guard with warning logs, and manages visited set cleanup on successful processing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
code/core/src/preview-api/modules/store/inferArgTypes.ts (2)

59-60: Consider caching before removing from visited for defensive correctness.

The current order — visited.delete then cache.set — is safe today because mapValues on line 55 is fully synchronous. However, the reverse order (cache.set first, then visited.delete) would be more defensively correct: a value in cache already has a complete result, so any re-entry would short-circuit cleanly. The current order creates a momentary window (between the two lines) where the value is neither in visited nor in cache, which could cause redundant recomputation if this code ever becomes async.

♻️ Suggested defensive ordering
-    visited.delete(value); // Remove from current path after processing
-    cache.set(value, result); // Cache the result for future lookups
+    cache.set(value, result); // Cache the result before removing from visited path
+    visited.delete(value); // Remove from current path after processing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/preview-api/modules/store/inferArgTypes.ts` around lines 59 -
60, Change the ordering when finalizing a computed result so the value is
inserted into the cache before it is removed from the recursion guard: call
cache.set(value, result) prior to visited.delete(value). This change in the
logic inside the function performing the mapValues traversal (the code around
mapValues and the visited/cache handling) ensures a fully computed result is
visible to re-entrancy checks and avoids the transient window where a value is
in neither visited nor cache, making the routine safe if it becomes
asynchronous.

67-73: The asymmetry between shared cache and per-arg new Set() is intentional and correct.

  • cache is shared across all top-level args, enabling cross-arg memoization for shared object references.
  • visited is fresh per arg so that a diamond-shaped reference (object appearing in two non-cyclic positions) is not falsely flagged as a cycle.

One consequence worth documenting: a cyclic object encountered in the first arg is cached with sentinel fields. Subsequent args referencing the same object return the cached result silently (no repeated warning). This is correct behavior — warn once — but worth noting in a code comment for future maintainers.

📝 Optional documentation comment
  const cache = new Map<any, SBType>();
+ // `cache` is shared across all args for cross-arg memoization. A fresh `visited` Set per
+ // arg prevents false cycle detection for shared (non-cyclic) object references.
  const argTypes = mapValues(initialArgs, (arg, key) => ({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/preview-api/modules/store/inferArgTypes.ts` around lines 67 -
73, Add a short explanatory comment in inferArgTypes next to the shared
cache/new Set() usage explaining that cache (Map named cache) is intentionally
shared across all top-level args to enable cross-arg memoization (so repeated
references return cached SBType), while the per-arg visited set (new Set()
passed into inferType) is freshly created for each arg to avoid false cycle
detection for diamond-shaped references; also note the consequence that a cyclic
object first encountered is cached with sentinel fields and subsequent
references will use the cached result silently (warn once).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@code/core/src/preview-api/modules/store/inferArgTypes.ts`:
- Around line 59-60: Change the ordering when finalizing a computed result so
the value is inserted into the cache before it is removed from the recursion
guard: call cache.set(value, result) prior to visited.delete(value). This change
in the logic inside the function performing the mapValues traversal (the code
around mapValues and the visited/cache handling) ensures a fully computed result
is visible to re-entrancy checks and avoids the transient window where a value
is in neither visited nor cache, making the routine safe if it becomes
asynchronous.
- Around line 67-73: Add a short explanatory comment in inferArgTypes next to
the shared cache/new Set() usage explaining that cache (Map named cache) is
intentionally shared across all top-level args to enable cross-arg memoization
(so repeated references return cached SBType), while the per-arg visited set
(new Set() passed into inferType) is freshly created for each arg to avoid false
cycle detection for diamond-shaped references; also note the consequence that a
cyclic object first encountered is cached with sentinel fields and subsequent
references will use the cached result silently (warn once).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca086e8 and 8e1bafb.

📒 Files selected for processing (1)
  • code/core/src/preview-api/modules/store/inferArgTypes.ts

@storybook-app-bot
Copy link
Copy Markdown

Package Benchmarks

Commit: 8e1bafb, ran on 25 February 2026 at 09:52:20 UTC

The following packages have significant changes to their size or dependencies:

@storybook/nextjs-vite

Before After Difference
Dependency count 92 92 0
Self size 1.12 MB 1.12 MB 🎉 -36 B 🎉
Dependency size 22.39 MB 22.46 MB 🚨 +72 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-native-web-vite

Before After Difference
Dependency count 124 124 0
Self size 30 KB 30 KB 0 B
Dependency size 23.68 MB 23.75 MB 🚨 +72 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-vite

Before After Difference
Dependency count 82 82 0
Self size 35 KB 35 KB 0 B
Dependency size 20.17 MB 20.25 MB 🚨 +72 KB 🚨
Bundle Size Analyzer Link Link

@storybook/vue3-vite

Before After Difference
Dependency count 108 108 0
Self size 35 KB 36 KB 🚨 +48 B 🚨
Dependency size 43.74 MB 43.77 MB 🚨 +33 KB 🚨
Bundle Size Analyzer Link Link

eslint-plugin-storybook

Before After Difference
Dependency count 20 20 0
Self size 131 KB 131 KB 0 B
Dependency size 3.42 MB 3.50 MB 🚨 +72 KB 🚨
Bundle Size Analyzer Link Link

Copy link
Copy Markdown
Member

@yannbf yannbf left a comment

Choose a reason for hiding this comment

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

LGTM

@valentinpalkovic valentinpalkovic added the patch:yes Bugfix & documentation PR that need to be picked to main branch label Feb 25, 2026
@valentinpalkovic valentinpalkovic merged commit bbca9fc into next Feb 25, 2026
133 of 145 checks passed
@valentinpalkovic valentinpalkovic deleted the valentin/fix-performance-in-infer-arg-types branch February 25, 2026 10:59
@github-actions github-actions Bot mentioned this pull request Feb 25, 2026
9 tasks
yannbf pushed a commit that referenced this pull request Feb 26, 2026
…n-infer-arg-types

Core: Avoid hanging when inferring args for recursive calls on DOM elemens
(cherry picked from commit bbca9fc)
@github-actions github-actions Bot added the patch:done Patch/release PRs already cherry-picked to main/release branch label Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal patch:done Patch/release PRs already cherry-picked to main/release branch patch:yes Bugfix & documentation PR that need to be picked to main branch

Projects

None yet

2 participants