Skip to content

React: Share a TypeScript DocumentRegistry across component-meta projects - #35468

Merged
valentinpalkovic merged 2 commits into
storybookjs:nextfrom
connorshea:perf/docgen-shared-document-registry
Aug 7, 2026
Merged

valentinpalkovic merged 2 commits into
storybookjs:nextfrom
connorshea:perf/docgen-shared-document-registry

Conversation

@connorshea

@connorshea connorshea commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

The changes in this PR were generated using Claude Code w/ Opus 4.8, I have verified the accuracy of its findings and benchmarks. I have reviewed the code myself and confirmed the accuracy of all claims here myself, and written this PR manually myself except where noted.

I'm opening this for consideration by the maintainers because I think this is a pretty big performance boost that is worth implementing. I don't have a strong preference for my PR being merged specifically, I'm 100% fine if this is just used as a proof-of-concept and a maintainer uses this concept for their own PR to actually implement this idea.

Note that #33909 did mention this optimization as a possibility, but it was never tested/implemented at the time.

It should also be noted that this may not be forward-compatible with the eventual TS 7.1 API which I presume will be used in the future. If we think that this changeset will potentially cause additional, undesired work for supporting TS 7.1 in the future, I'm also fine closing this PR on those grounds. But I doubt that should be a serious concern.

What I did

Basically, the ComponentMetaManager was creating one language service for each tsconfig.json file. That doesn't matter for a project with only one tsconfig, but will result in a lot of duplicated DocumentRegistry instances in a monorepo. TypeScript creates a private registry for each language service if you don't explicitly provide one, so the lib.d.ts, React's types, etc. were being duplicated for each language service. We can improve the runtime of the docgen server by sharing the DocumentRegistry across the separate language service instances.

This has no negative impact on single-tsconfig repos as the DocumentRegistry is created once, just like it was before on a single-tsconfig repo.

This won't have any performance improvement if different projects in a monorepo have entirely different tsconfig settings, but it still works fine and doesn't have any downside for a repo like that.

Measured on this repo's own Storybook (188 components across 7 projects, 20,657 SourceFiles of which only 4,763 are unique to the largest project).

Comparison of SourceFile objects before vs after:

next this PR
TS programs 7 7
SourceFile slots (sum across programs) 20,657 20,657
distinct SourceFile objects 20,657 9,635
distinct file paths 5,607 5,607

Static build time, storybook build with experimentalDocgenServer on, three runs per branch:

next     15.24s  14.37s  15.44s
this PR  11.40s  11.01s  11.43s   (~25% faster)

The live JS heap is sampled in-process via a benchmark harness written with Claude (note that this doesn't matter as much for overall memory usage, see caveat below):

extraction time   7.17s   -> 5.12s   (-29%)
peak heap used    3507 MB -> 1929 MB (-45%)

The output is identical to the output of next.

Best as I've been able to tell in the benchmarking, there is no difference in the peak RSS of the storybook build process overall thanks to this PR, only the live heap used by the docgen server is reduced.

This should still help with the OOM issues (see #35260) in monorepos, because the heap-pressure recycler from #35305 is tripped less often with this change. Testing the same 188 components under a few different V8 heap limits:

V8 heap limit recycle threshold (70%) next: peak heap / recycles this PR: peak heap / recycles
3264 MB 2285 MB 2318 MB / 1 2008 MB / 0
4288 MB 3002 MB 3481 MB / 2 2000 MB / 0
6336 MB 4435 MB 3509 MB / 0 1999 MB / 0

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

There is one additional test added here to verify that the shared DocumentRegistry is actually being used. The existing test suite should also verify that nothing else broke with this change.

Manual testing

This bash script (written by Claude) can be used to verify that the build runtime is improved on this branch vs. the original

run_arm () {
  local ref=$1
  git checkout -q "$ref"
  yarn nx compile react --skip-nx-cache >/dev/null 2>&1

  if grep -rqi documentRegistry code/renderers/react/dist/; then
    echo "### $ref -> dist HAS the fix"
  else
    echo "### $ref -> dist does NOT have the fix"
  fi

  cd code
  for i in 1 2 3; do
    rm -rf "./sb-$ref-$i"
    STORYBOOK_EXPERIMENTAL_DOCGEN_SERVER=true STORYBOOK_DISABLE_TELEMETRY=1 \
      node core/dist/bin/dispatcher.js build --config-dir ./.storybook --output-dir "./sb-$ref-$i"
  done
  cd ..
}

run_arm next
run_arm <THIS-BRANCH>

After running the above, you can also diff the two docgen outputs to ensure that they are identical between this branch and next (note that you may need to ensure that this branch is rebased off latest next if any changes land before this gets tested):

diff -r code/sb-next-2/services/core/docgen code/sb-<THIS-BRANCH>-2/services/core/docgen

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

  • Declare whether manual QA will be needed for this PR during the next release, through qa:needed or qa:skip

  • 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>

@connorshea
connorshea marked this pull request as ready for review July 12, 2026 00:30
@coderabbitai

coderabbitai Bot commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dd9a8e9-ca72-4997-a6dc-ad1461b02d55

📥 Commits

Reviewing files that changed from the base of the PR and between 991596a and ef9e0e1.

📒 Files selected for processing (3)
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaManager.test.ts
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaManager.ts
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaManager.test.ts
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts

Walkthrough

ComponentMetaManager shares a TypeScript DocumentRegistry across configured and inferred projects. ComponentMetaProject uses the registry for language services and exposes getSourceFile. A test verifies shared SourceFile identity across projects.

Changes

Shared document registry

Layer / File(s) Summary
Registry creation and project wiring
code/renderers/react/src/componentManifest/componentMeta/ComponentMetaManager.ts, code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts
A case-sensitivity-aware TypeScript DocumentRegistry is passed to configured and inferred projects, and the project stores it for language service creation.
SourceFile access and reuse validation
code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts, code/renderers/react/src/componentManifest/componentMeta/ComponentMetaManager.test.ts
getSourceFile retrieves program source files, hasSourceFile uses it, and the test verifies identical shared-file instances across projects.

Possibly related PRs

  • storybookjs/storybook#35657: Extends the extracted ComponentMetaProject and ComponentMetaManager implementation with shared DocumentRegistry handling and source-file identity tests.

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.

@github-actions github-actions Bot added the Stale label Jul 22, 2026
@connorshea
connorshea force-pushed the perf/docgen-shared-document-registry branch from 023027a to 19ebcbe Compare July 23, 2026 04:12
@connorshea
connorshea requested a review from a team July 23, 2026 04:12
@valentinpalkovic valentinpalkovic added the sev:S3 Medium priority. Fix within months if possible. label Jul 31, 2026
@connorshea
connorshea force-pushed the perf/docgen-shared-document-registry branch from 19ebcbe to 201ee0a Compare August 3, 2026 14:01
@connorshea

Copy link
Copy Markdown
Contributor Author

@valentinpalkovic I've rebased to resolve merge conflicts that showed up and re-benchmarked to ensure the change still improves perf (it does). This is my first proper contribution to Storybook, is there anything further I should do to make sure this is ready to go, or am I good to wait for a review now? :)

@valentinpalkovic valentinpalkovic added the ci:normal Run our default set of CI jobs (choose this for most PRs). label Aug 3, 2026
@valentinpalkovic valentinpalkovic self-assigned this Aug 3, 2026
@connorshea

Copy link
Copy Markdown
Contributor Author

Looks like there's a separate problem in the CI config causing the failure for the build-linux job

…ects

ComponentMetaManager creates one ComponentMetaProject (and therefore one TS
LanguageService) per matched tsconfig, which in a monorepo means one per
package. Each LanguageService was created without a DocumentRegistry, so TS gave
each one a private registry and every project parsed and bound its own copy of
lib.d.ts, React's types and node_modules. The shared fsFileSnapshots cache only
deduplicated the file reads, not the resulting ASTs.

Passing a single registry down from the manager lets projects with matching
compiler options reuse those SourceFiles. Measured on this repo's own Storybook
(188 components across 7 projects, 20,657 SourceFiles of which only 4,763 are
unique to the largest project):

  extraction    7.17s   -> 5.12s   (-29%)
  peak heap     3507 MB -> 1929 MB (-45%)
  static build  13.2s   -> 10.4s   (-21%)

Extracted docgen output is byte-identical before and after.

The lower heap ceiling also relieves the OOM that RECYCLE_HEAP_PRESSURE_RATIO
guards against (storybookjs#35260): programs are recycled less often, so the rebuild cost
is paid less often. Recycling does not need to clear the registry: disposing a
LanguageService releases every SourceFile it holds, and the registry evicts an
entry once its last reference goes, so disposing the projects already empties
it. Clearing it explicitly would free nothing and only throw away sharing with
any project that outlived the recycle.

createDocumentRegistry() defaults useCaseSensitiveFileNames to false, which
lowercases the path keys it canonicalizes — wrong on a case-sensitive
filesystem. It happens to be harmless today because a LanguageService only ever
reaches the registry through the *WithKey APIs, passing a path it already
canonicalized against its own host, so the registry's own value is never read.
That is a fragile thing to rely on, so pass the host's value explicitly.
currentDirectory stays at its default for the same reason: it is never consulted
via the *WithKey path, and no single value could be correct for every project.

The test asserts that two projects under different tsconfigs hand back the same
SourceFile instance for a file they both reach. Identity is the only thing worth
asserting here: sharing the registry leaves the extracted docgen
byte-identical, so no output-level test can tell the two apart, and the existing
docgen-memory gate generates a single-tsconfig project and is therefore blind to
it. getSourceFile() is factored out of hasSourceFile(), which already went
through the program to answer the same question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@connorshea
connorshea force-pushed the perf/docgen-shared-document-registry branch from 201ee0a to 20abff1 Compare August 3, 2026 18:16
@valentinpalkovic valentinpalkovic added the qa:needed Pull Requests that will need manual QA prior to release. label Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@valentinpalkovic
valentinpalkovic merged commit 7fecff4 into storybookjs:next Aug 7, 2026
119 of 127 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 7, 2026
2 tasks done
@ndelangen ndelangen added qa:success Pull Requests that were successfully QA'ed by the release team. and removed qa:needed Pull Requests that will need manual QA prior to release. labels Aug 28, 2026
@storybook-app-bot

Copy link
Copy Markdown
Contributor

Package Benchmarks

Commit: ef9e0e1, ran on 28 August 2026 at 14:17:32 UTC

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

@storybook/addon-mcp

Before After Difference
Dependency count 11 13 🚨 +2 🚨
Self size 108 KB 191 KB 🚨 +83 KB 🚨
Dependency size 2.74 MB 2.89 MB 🚨 +145 KB 🚨
Bundle Size Analyzer Link Link

storybook-addon-pseudo-states

Before After Difference
Dependency count 0 0 0
Self size 24 KB 21 KB 🎉 -3 KB 🎉
Dependency size 686 B 689 B 🚨 +3 B 🚨
Bundle Size Analyzer Link Link

@storybook/addon-vitest

Before After Difference
Dependency count 2 2 0
Self size 466 KB 429 KB 🎉 -37 KB 🎉
Dependency size 350 KB 350 KB 🚨 +3 B 🚨
Bundle Size Analyzer Link Link

@storybook/builder-vite

Before After Difference
Dependency count 1 11 🚨 +10 🚨
Self size 136 KB 136 KB 🎉 -382 B 🎉
Dependency size 28 KB 1.33 MB 🚨 +1.30 MB 🚨
Bundle Size Analyzer Link Link

@storybook/builder-webpack5

Before After Difference
Dependency count 182 182 0
Self size 92 KB 79 KB 🎉 -13 KB 🎉
Dependency size 37.43 MB 37.43 MB 🚨 +9 B 🚨
Bundle Size Analyzer Link Link

storybook

Before After Difference
Dependency count 73 73 0
Self size 22.04 MB 21.45 MB 🎉 -596 KB 🎉
Dependency size 31.20 MB 30.98 MB 🎉 -218 KB 🎉
Bundle Size Analyzer Link Link

@storybook/angular

Before After Difference
Dependency count 185 185 0
Self size 267 KB 252 KB 🎉 -14 KB 🎉
Dependency size 30.29 MB 30.28 MB 🎉 -13 KB 🎉
Bundle Size Analyzer Link Link

@storybook/angular-vite

Before After Difference
Dependency count 20 29 🚨 +9 🚨
Self size 23.08 MB 23.04 MB 🎉 -43 KB 🎉
Dependency size 11.49 MB 12.69 MB 🚨 +1.21 MB 🚨
Bundle Size Analyzer Link Link

@storybook/ember

Before After Difference
Dependency count 181 181 0
Self size 13 KB 13 KB 🚨 +3 B 🚨
Dependency size 32.79 MB 32.78 MB 🎉 -13 KB 🎉
Bundle Size Analyzer Link Link

@storybook/html-vite

Before After Difference
Dependency count 4 14 🚨 +10 🚨
Self size 22 KB 22 KB 🎉 -8 B 🎉
Dependency size 199 KB 1.50 MB 🚨 +1.30 MB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs

Before After Difference
Dependency count 536 527 🎉 -9 🎉
Self size 642 KB 641 KB 🎉 -414 B 🎉
Dependency size 64.69 MB 64.13 MB 🎉 -564 KB 🎉
Bundle Size Analyzer Link Link

@storybook/nextjs-vite

Before After Difference
Dependency count 101 94 🎉 -7 🎉
Self size 1.42 MB 1.42 MB 🎉 -965 B 🎉
Dependency size 23.32 MB 23.89 MB 🚨 +564 KB 🚨
Bundle Size Analyzer Link Link

@storybook/preact-vite

Before After Difference
Dependency count 4 14 🚨 +10 🚨
Self size 12 KB 12 KB 🎉 -8 B 🎉
Dependency size 217 KB 1.52 MB 🚨 +1.30 MB 🚨
Bundle Size Analyzer Link Link

@storybook/react-native-web-vite

Before After Difference
Dependency count 122 125 🚨 +3 🚨
Self size 29 KB 29 KB 🚨 +23 B 🚨
Dependency size 25.71 MB 25.90 MB 🚨 +188 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-vite

Before After Difference
Dependency count 79 83 🚨 +4 🚨
Self size 32 KB 32 KB 🎉 -263 B 🎉
Dependency size 20.44 MB 21.19 MB 🚨 +754 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 268 268 0
Self size 23 KB 23 KB 🚨 +17 B 🚨
Dependency size 49.88 MB 49.87 MB 🎉 -11 KB 🎉
Bundle Size Analyzer Link Link

@storybook/server-webpack5

Before After Difference
Dependency count 194 194 0
Self size 15 KB 15 KB 🎉 -7 B 🎉
Dependency size 38.71 MB 38.69 MB 🎉 -13 KB 🎉
Bundle Size Analyzer Link Link

@storybook/svelte-vite

Before After Difference
Dependency count 11 20 🚨 +9 🚨
Self size 54 KB 54 KB 🚨 +16 B 🚨
Dependency size 25.47 MB 26.67 MB 🚨 +1.20 MB 🚨
Bundle Size Analyzer Link Link

@storybook/sveltekit

Before After Difference
Dependency count 12 21 🚨 +9 🚨
Self size 56 KB 56 KB 🚨 +5 B 🚨
Dependency size 25.52 MB 26.73 MB 🚨 +1.20 MB 🚨
Bundle Size Analyzer Link Link

@storybook/tanstack-react

Before After Difference
Dependency count 80 84 🚨 +4 🚨
Self size 132 KB 114 KB 🎉 -18 KB 🎉
Dependency size 20.47 MB 21.23 MB 🚨 +754 KB 🚨
Bundle Size Analyzer Link Link

@storybook/vue3-vite

Before After Difference
Dependency count 94 102 🚨 +8 🚨
Self size 32 KB 31 KB 🎉 -228 B 🎉
Dependency size 18.54 MB 19.56 MB 🚨 +1.02 MB 🚨
Bundle Size Analyzer Link Link

@storybook/web-components-vite

Before After Difference
Dependency count 5 15 🚨 +10 🚨
Self size 19 KB 19 KB 🚨 +4 B 🚨
Dependency size 266 KB 1.56 MB 🚨 +1.30 MB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 205 205 0
Self size 869 KB 831 KB 🎉 -38 KB 🎉
Dependency size 87.61 MB 86.79 MB 🎉 -825 KB 🎉
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 198 198 0
Self size 44 KB 32 KB 🎉 -12 KB 🎉
Dependency size 86.08 MB 85.26 MB 🎉 -814 KB 🎉
Bundle Size Analyzer Link Link

create-storybook

Before After Difference
Dependency count 74 74 0
Self size 1.09 MB 1.09 MB 🚨 +1 KB 🚨
Dependency size 53.24 MB 52.43 MB 🎉 -814 KB 🎉
Bundle Size Analyzer node node

@storybook/csf-plugin

Before After Difference
Dependency count 0 9 🚨 +9 🚨
Self size 0 B 7 KB 🚨 +7 KB 🚨
Dependency size 0 B 1.29 MB 🚨 +1.29 MB 🚨
Bundle Size Analyzer Link Link

@storybook/mcp

Before After Difference
Dependency count 11 11 0
Self size 151 KB 107 KB 🎉 -43 KB 🎉
Dependency size 2.74 MB 2.74 MB 🚨 +2 B 🚨
Bundle Size Analyzer Link Link

@storybook/vue3

Before After Difference
Dependency count 90 90 0
Self size 184 KB 98 KB 🎉 -85 KB 🎉
Dependency size 18.11 MB 18.11 MB 🎉 -37 B 🎉
Bundle Size Analyzer Link Link

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

Labels

agent-scan:human ci:normal Run our default set of CI jobs (choose this for most PRs). maintenance User-facing maintenance tasks needs triage performance issue qa:success Pull Requests that were successfully QA'ed by the release team. react sev:S3 Medium priority. Fix within months if possible.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants