Skip to content

Vite: Use vite hook filter for performance improvements#33900

Closed
huang-julien wants to merge 3 commits into
storybookjs:mainfrom
huang-julien:perf/plugin-hook-filter
Closed

Vite: Use vite hook filter for performance improvements#33900
huang-julien wants to merge 3 commits into
storybookjs:mainfrom
huang-julien:perf/plugin-hook-filter

Conversation

@huang-julien
Copy link
Copy Markdown
Contributor

@huang-julien huang-julien commented Feb 21, 2026

Closes #33899

What I did

This PR moves Vite plugins to use HookObjects instead of functions.

For example transform(code, id) would move to

{ 
  transform: {
    filter: { id, code },
    handler(code, id) {}
  }
}

This allows to reduce the overhead between JS/Rust runtimes for vite 8 with rolldown. Implementation within handlers doesn't change, filter() are left within to keep plugins backward compatible with vite < 6.3

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 PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

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

Summary by CodeRabbit

  • Refactor
    • Restructured internal plugin transformation APIs for consistency across multiple build and framework plugins, maintaining existing functionality with no impact on end-user experience.

@dosubot
Copy link
Copy Markdown

dosubot Bot commented Feb 21, 2026

Related Documentation

Checked 0 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

This PR refactors Vite plugin implementations across the codebase to adopt a structured API for transform, resolveId, and load hooks. Instead of direct function implementations, plugins now use objects with filter and handler properties for pre-filtering and selective processing. The processing logic and observable behavior remain unchanged in most cases.

Changes

Cohort / File(s) Summary
Builder Vite Plugin Hooks
code/builders/builder-vite/src/plugins/code-generator-plugin.ts, inject-export-order-plugin.ts, external-globals-plugin.ts, strip-story-hmr-boundaries.ts
Refactored resolveId and transform hooks from functions to structured objects with filter and handler. Added escape regex and filter patterns for virtual modules. External globals plugin adds code-filtered transform to optimize processing by checking for external keys in code.
Vite Mocker and Mock Plugins
code/builders/builder-vite/src/plugins/vite-inject-mocker/plugin.ts, vite-mock/plugin.ts
Converted resolveId and transform hooks from functions to filter/handler objects, narrowing applicability via regex-based filters while preserving existing logic.
Framework Docgen Plugins
code/frameworks/react-vite/src/plugins/react-docgen.ts, svelte-vite/src/plugins/svelte-docgen.ts, vue3-vite/src/plugins/vue-docgen.ts
Restructured transform hooks into filter/handler objects. React and Svelte plugins preserve parsing and generation logic within handlers. Vue docgen maintains MagicString manipulation and return format unchanged.
Vue Component Meta Plugin
code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts
Converted transform hook to filter/handler structure with additional filtering logic: deduplicates exposed entries, removes spurious \$slots exposes, and adds safeguards to prevent annotating re-exported identifiers. Enhanced inline filtering and metadata application logic.
Addon MDX Plugin
code/addons/docs/src/mdx-plugin.ts
Refactored transform hook from direct async function to filter/handler object while preserving MDX validation, loader options building, and compilation logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • storybookjs/storybook#33819: Modifies the same Vite plugin files (code-generator-plugin, inject-export-order-plugin, external-globals-plugin, strip-story-hmr-boundaries) with overlapping hook refactoring patterns.
  • storybookjs/storybook#32751: Modifies code/frameworks/react-vite/src/plugins/react-docgen.ts, the same file undergoing transform hook refactoring in this PR.
✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
code/frameworks/react-vite/src/plugins/react-docgen.ts (1)

82-82: Use === instead of == for definedInFile comparison.

definedInFile == id uses loose equality. Since both values are strings, this works, but strict equality (===) is the standard practice in TypeScript codebases and avoids subtle type-coercion surprises.

Proposed fix
-            if (actualName && definedInFile == id) {
+            if (actualName && definedInFile === id) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frameworks/react-vite/src/plugins/react-docgen.ts` at line 82, Replace
the loose equality in the react-docgen plugin conditional: change the comparison
in the if statement that checks actualName and definedInFile against id from
`definedInFile == id` to strict equality `definedInFile === id`; update the
condition `if (actualName && definedInFile == id)` to use `===` so the check
involving actualName, definedInFile and id is type-safe.
code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts (1)

126-128: Static analysis flags ReDoS on dynamically constructed RegExp — low risk here.

The name variable originates from checker.getExportNames(id), which yields JS identifiers (not user-supplied input), so the practical ReDoS risk is negligible. This is pre-existing code, not introduced by this PR, but worth noting for future hardening — e.g., escaping name with a regex-escape utility before interpolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts` around lines 126
- 128, The dynamic RegExp construction using the identifier variable name (from
checker.getExportNames(id)) can cause a ReDoS concern; update the two RegExp
constructions that interpolate ${name} (the ones testing src with patterns like
`export {.*${name}.*}` and `export \* from ['"]\S*${name}['"]`) to escape any
regex-special characters in name before interpolation (e.g., use a shared
escapeRegExp utility or a safe escape function) so the generated patterns are
safe even if name contains metacharacters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/builders/builder-vite/src/plugins/code-generator-plugin.ts`:
- Around line 79-103: The load filter currently only matches the virtual IDs
(via filter: { id: /\0virtual:\/@storybook\/builder-vite\// }) so the iframeId
branch in the handler is never invoked; replace the filter+handler pair with a
single load(id) function (or broaden the filter to include iframeId) that
explicitly checks id against getResolvedVirtualModuleId(SB_VIRTUAL_FILES.*) and
iframeId (which is set in configResolved) before returning content—update the
plugin to use load(id) { switch(id) { case getResolvedVirtualModuleId(...): ...;
case iframeId: ... } } so iframe.html is actually served.

In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts`:
- Around line 110-112: Remove the duplicated comment "if there is no component
meta, return undefined" in vue-component-meta.ts so only a single instance
remains; locate the repeated comment near the component meta handling logic in
the plugin (the block that checks for component meta) and delete the redundant
line to avoid duplicate comments while preserving the single explanatory
comment.

In `@code/frameworks/vue3-vite/src/plugins/vue-docgen.ts`:
- Around line 13-14: Update the Vite transform hook's filter to use the
documented object format: instead of passing the RegExp directly as `id:
include`, wrap it as `id: { include }` so the transform config matches the
expected `{ id: { include, exclude } }` shape (mirror the usage in
svelte-docgen's transform filter); modify the `transform` config where `filter`
is defined and replace `filter: { id: include }` with `filter: { id: { include }
}`.

---

Nitpick comments:
In `@code/frameworks/react-vite/src/plugins/react-docgen.ts`:
- Line 82: Replace the loose equality in the react-docgen plugin conditional:
change the comparison in the if statement that checks actualName and
definedInFile against id from `definedInFile == id` to strict equality
`definedInFile === id`; update the condition `if (actualName && definedInFile ==
id)` to use `===` so the check involving actualName, definedInFile and id is
type-safe.

In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts`:
- Around line 126-128: The dynamic RegExp construction using the identifier
variable name (from checker.getExportNames(id)) can cause a ReDoS concern;
update the two RegExp constructions that interpolate ${name} (the ones testing
src with patterns like `export {.*${name}.*}` and `export \* from
['"]\S*${name}['"]`) to escape any regex-special characters in name before
interpolation (e.g., use a shared escapeRegExp utility or a safe escape
function) so the generated patterns are safe even if name contains
metacharacters.

Comment on lines 79 to 103
load: {
filter: { id: /\0virtual:\/@storybook\/builder-vite\// },
async handler(id) {
switch (id) {
case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_STORIES_FILE): {
const storyIndexGenerator = await storyIndexGeneratorPromise;
const index = await storyIndexGenerator?.getIndex();
return generateImportFnScriptCode(index);
}

case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_ADDON_SETUP_FILE): {
return generateAddonSetupCode();
}
case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_APP_FILE): {
return generateModernIframeScriptCode(options, projectRoot);
case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_ADDON_SETUP_FILE): {
return generateAddonSetupCode();
}
case getResolvedVirtualModuleId(SB_VIRTUAL_FILES.VIRTUAL_APP_FILE): {
return generateModernIframeScriptCode(options, projectRoot);
}
case iframeId: {
return readFileSync(
fileURLToPath(importMetaResolve('@storybook/builder-vite/input/iframe.html')),
'utf-8'
);
}
}
case iframeId: {
return readFileSync(
fileURLToPath(importMetaResolve('@storybook/builder-vite/input/iframe.html')),
'utf-8'
);
}
}
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

load filter excludes iframeId — the case iframeId branch is unreachable.

The filter at line 80 only matches IDs containing \0virtual:/@storybook/builder-vite/, but iframeId is set to ${config.root}/iframe.html (line 64), which doesn't contain that prefix. Vite's native filter will skip calling the handler for iframeId entirely, so lines 95–100 are dead code and the iframe HTML content will never be served by this plugin.

The filter needs to also match iframeId. Since iframeId is only known after configResolved, consider broadening the filter:

Proposed fix
     load: {
-      filter: { id: /\0virtual:\/@storybook\/builder-vite\// },
+      filter: { id: [/\0virtual:\/@storybook\/builder-vite\//, /iframe\.html/] },
       async handler(id) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/builders/builder-vite/src/plugins/code-generator-plugin.ts` around lines
79 - 103, The load filter currently only matches the virtual IDs (via filter: {
id: /\0virtual:\/@storybook\/builder-vite\// }) so the iframeId branch in the
handler is never invoked; replace the filter+handler pair with a single load(id)
function (or broaden the filter to include iframeId) that explicitly checks id
against getResolvedVirtualModuleId(SB_VIRTUAL_FILES.*) and iframeId (which is
set in configResolved) before returning content—update the plugin to use
load(id) { switch(id) { case getResolvedVirtualModuleId(...): ...; case
iframeId: ... } } so iframe.html is actually served.

Comment on lines +110 to +112
// if there is no component meta, return undefined

const hasEvent = meta.events.find((event) => event.name === nameWithoutOnPrefix);
return !hasEvent;
})
// remove unwanted duplicated "$slots" expose
.filter((expose) => {
if (expose.name === '$slots') {
const slotNames = meta.slots.map((slot) => slot.name);
return !slotNames.every((slotName) => expose.type.includes(slotName));
}
return true;
});
// if there is no component meta, return undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate comment.

Line 112 is an exact duplicate of line 110.

Proposed fix
           // if there is no component meta, return undefined
-
-          // if there is no component meta, return undefined
           if (metaSources.length === 0) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// if there is no component meta, return undefined
const hasEvent = meta.events.find((event) => event.name === nameWithoutOnPrefix);
return !hasEvent;
})
// remove unwanted duplicated "$slots" expose
.filter((expose) => {
if (expose.name === '$slots') {
const slotNames = meta.slots.map((slot) => slot.name);
return !slotNames.every((slotName) => expose.type.includes(slotName));
}
return true;
});
// if there is no component meta, return undefined
// if there is no component meta, return undefined
if (metaSources.length === 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts` around lines 110
- 112, Remove the duplicated comment "if there is no component meta, return
undefined" in vue-component-meta.ts so only a single instance remains; locate
the repeated comment near the component meta handling logic in the plugin (the
block that checks for component meta) and delete the redundant line to avoid
duplicate comments while preserving the single explanatory comment.

Comment thread code/frameworks/vue3-vite/src/plugins/vue-docgen.ts
@huang-julien huang-julien marked this pull request as draft February 21, 2026 15:58
@yannbf yannbf added performance issue ci:daily Run the CI jobs that normally run in the daily job. builder-vite labels Mar 3, 2026
@yannbf yannbf changed the title perf: use vite hook filter Vite: Use vite hook filter for performance improvements Mar 3, 2026
@huang-julien huang-julien force-pushed the perf/plugin-hook-filter branch from 2a92ec8 to 2a76f88 Compare March 3, 2026 14:13
@storybook-app-bot
Copy link
Copy Markdown

Package Benchmarks

Commit: 40465f4, ran on 3 March 2026 at 15:11:08 UTC

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

@storybook/builder-webpack5

Before After Difference
Dependency count 188 185 🎉 -3 🎉
Self size 76 KB 76 KB 0 B
Dependency size 32.23 MB 32.18 MB 🎉 -46 KB 🎉
Bundle Size Analyzer Link Link

@storybook/angular

Before After Difference
Dependency count 188 185 🎉 -3 🎉
Self size 139 KB 139 KB 0 B
Dependency size 30.44 MB 30.40 MB 🎉 -46 KB 🎉
Bundle Size Analyzer Link Link

@storybook/ember

Before After Difference
Dependency count 192 189 🎉 -3 🎉
Self size 15 KB 15 KB 0 B
Dependency size 28.94 MB 28.89 MB 🎉 -46 KB 🎉
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 274 271 🎉 -3 🎉
Self size 24 KB 24 KB 0 B
Dependency size 44.60 MB 44.56 MB 🎉 -46 KB 🎉
Bundle Size Analyzer Link Link

@storybook/server-webpack5

Before After Difference
Dependency count 200 197 🎉 -3 🎉
Self size 16 KB 16 KB 0 B
Dependency size 33.48 MB 33.44 MB 🎉 -46 KB 🎉
Bundle Size Analyzer Link Link

@storybook/preset-react-webpack

Before After Difference
Dependency count 170 167 🎉 -3 🎉
Self size 18 KB 18 KB 0 B
Dependency size 31.44 MB 31.39 MB 🎉 -46 KB 🎉
Bundle Size Analyzer Link Link

@huang-julien
Copy link
Copy Markdown
Contributor Author

Cloosing as started to work on wrong branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

builder-vite ci:daily Run the CI jobs that normally run in the daily job. performance issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants