Repository navigation
React: Share a TypeScript DocumentRegistry across component-meta projects - #35468
valentinpalkovic merged 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
Walkthrough
ChangesShared document registry
Possibly related PRs
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. Comment |
023027a to
19ebcbe
Compare
19ebcbe to
201ee0a
Compare
|
@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? :) |
|
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>
201ee0a to
20abff1
Compare
|
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. |
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
| 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 |
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.jsonfile. 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 thelib.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:
Static build time,
storybook buildwithexperimentalDocgenServeron, three runs per branch: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):
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 buildprocess 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:
Checklist for Contributors
Testing
The changes in this PR are covered in the following automated 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
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 latestnextif any changes land before this gets tested):Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal,ci:mergedorci:dailyGH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.tsDeclare whether manual QA will be needed for this PR during the next release, through
qa:neededorqa:skipMake 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/coreteam 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>