Optimizing TurboSnap for Monorepos
For large teams using a monorepo and managing changes across dozens of packages, it’s crucial not just to run tests faster but to run the right ones. TurboSnap excels in these scenarios by accelerating test runs, executing UI tests only on what has actually changed. However, poorly structured dependencies can derail your whole setup. Constantly triggering full rebuilds or skipping coverage on affected components makes it impossible to trust the results.
In this guide, we’ll break down practical strategies for managing dependencies
, devDependencies
, dynamic imports, and package.json
files in a way that keeps your TurboSnap builds snappy and your coverage meaningful.
✨ Try TurboSnap Helper! Run our helper utility to accurately configure TurboSnap, even in monorepos—no more guess work! Read how you can run the utility and get instant help with your config.
Prefer importing from built packages over src
Do you have components that are part of a design system or shared library? If so, import them from the built package (ex. from node_modules/@your-org/ui
). Avoid importing directly from src
unless you want the story to retest when the imported file changes. This is because TurboSnap uses git tracking to detect changes. Built packages are usually excluded from git (ex. dist
is in .gitignore
), so they won’t trigger unnecessary story rebuilds unless the consumer has actually rebuilt and re-imported them.
Some teams intentionally import from src
for faster development. If this is the case, consider the tradeoffs and whether faster development or fewer unnecessary tests are more important to the team.
Use project-specific lockfiles carefully in monorepos
When using tools like Nx or pnpm workspaces with project-specific lockfiles, be aware that changes to a package.json
file (without a valid lockfile) or modifications to lockfiles can trigger full rebuilds.
With Nx, you can mitigate this by using implicitDependencies
in your Nx project configuration. This allows you to specify which stories are affected when a package file changes, reducing the scope of rebuilds.
Although TurboSnap doesn’t have native awareness of affected files, you can use nx affected
(or an equivalent tool) to determine the need to rebuild. This approach leads to more targeted tests and minimizes unnecessary full rebuilds.
Avoid changes to shared or root-level package files unless necessary
Changing dependencies at the root or in shared package.json
files will often trigger TurboSnap to retest all stories. When possible, limit root-level package.json
changes to toolchains or devDependencies. Keep shared runtime dependencies in leaf packages (a standalone UI component or package that isn’t imported by any other internal packages) so they’re not a dependency for any other package.
Use devDependencies
to avoid unnecessary rebuilds
TurboSnap uses your bundler’s (Webpack, Vite, Rsbuild, etc.) dependency graph to determine which stories are affected by changes. That means changes to runtime dependencies (dependencies
) can ripple into the UI, while development-only tools (devDependencies
) often don’t, unless they’re used in your Storybook config or components.
We recommend:
- Keeping runtime dependencies (like
react
,styled-components
,axios
) independencies
. - Using
devDependencies
for build tools (ex.vite
,eslint
,typescript
, Storybook addons). - Avoid using dev-only packages in
preview.ts
or inside components—if they’re needed at runtime, they should be independencies
.
Group and isolate high-churn utilities
If you have shared utility functions that frequently change, you may see more retests than expected. Every change to those utility files will cascade to every component that imports from the package, which can trigger TurboSnap to retest all related stories.
Instead, isolate those utilities into a clearly scoped package and avoid re-exporting them through your UI library or core package. Import the utilities in your app code and not your shared design system unless they’re directly UI-related.
Avoid dynamic imports in preview files
Dynamic imports can disrupt TurboSnap’s static analysis of your dependency graph. Since paths are resolved at runtime, it’s best to avoid them in story files unless absolutely necessary. That’s because it prevents TurboSnap from accurately tracing changes, and using dynamic imports in your preview file can trigger unexpected rebuilds.
To ensure full traceability, always use direct imports in your preview and shared utility files.
Why does this happen? Dynamic imports break the chain TurboSnap relies on to identify affected stories. This can lead to either under-testing or over-testing. Direct imports ensure that the dependency graph can follow the file, identifying which components are affected. As a result, TurboSnap only retests stories that directly or indirectly depend on the modified file.
Avoid excessive wrapper indirection
You can’t avoid full rebuilds when changing anything imported by preview.ts
, but you reduce how often it happens and improve your team’s visibility by avoiding excessive wrapper indirection.
Excessive wrapper indirection may look like withTheme()
wrapping stories with a provider theme, then being imported by withGlobalProvider()
which wraps the stories in locale. Flattening this structure so withTheme()
and withLocale()
are individually defined makes it easier to see which dependencies are responsible when retesting. It also lets you bypass the preview file for some stories by importing the providers directly in specific stories, which would ensure only those files are tested when changes are made to the providers.
When possible, use direct imports and consider moving stable wrappers into preview.ts
. If you’re using frequently changing wrappers or features, try to limit them to individual story files to minimize their impact.
Document your dependency graph
Larger teams working out of a monorepo can benefit from clearly identifying which packages depend on which. Tools like Nx Graph (or similar) can help your team visualize your dependency graph and avoid unintentional coupling between packages.
Monitor changedPackageFiles
in Chromatic builds
If you notice frequent rebuilds due to changes in package files, keep an eye on Chromatic’s CLI output for changedPackageFiles
. TurboSnap will list all package files that triggered a rebuild. Investigate whether the changes are necessary, if there are opportunities to better isolate your packages, or if you should consider using --untraced
to ignore the packages.
Use caution when marking package.json
files as untraced
Monorepos often include multiple package.json
files, and some may trigger rebuilds unnecessarily. You can use --untraced
to ignore these files. Use the following guidelines to determine if it’s safe to mark a package.json
file as untraced:
✅ It’s safe to mark package.json
as untraced when:
- The package is not consumed by Storybook or preview files
- The file does not affect the runtime or build output (ex. purely backend or tooling packages)
- You use a shared lockfile (
pnpm-lock.yaml
,yarn.lock
), ensuring consistent resolution - You rely on
Nx affected
or similar tooling
❌ Do not untrace if:
- The package includes components, themes, or utilities used in stories
- The package influences global configuration or decorators in
preview.js|ts
- Changes to it could result in different builds or runtime behavior
Conclusion
Smart dependency management in a monorepo can drastically improve TurboSnap performance and confidence, making it easier and faster to test changes before pushing them to production. By being intentional with how you structure dependencies
, avoiding dynamic imports in shared config, and tracing changes clearly, you’ll strike the perfect balance between speed and reliability in your visual testing pipeline.
Ready to optimize your builds? Start by auditing your most frequently rebuilt packages!
Looking to get more details on dependency tracing with TurboSnap? Head over to our docs to read more about TurboSnap dependency tracing.