Optimizing Monorepo Test Performance with Vitest Workspaces
Use Vitest Workspaces to run multiple project test configs under one runner in a monorepo, cutting memory overhead while keeping environments isolated.
23 Oct 2025, 14:53 UTC

Running a separate Vitest instance for every sub-package in a monorepo leads to duplicated dependency loading, high memory overhead, and slow feedback cycles. The fix is Vitest Workspaces: a single runner instance that aggregates multiple project configurations, shares one task graph and coordinator process, and still keeps each project's environment isolated.
The Problem with Isolated Runners
When you run vitest separately in each package directory, every instance initializes its own environment, loads duplicate copies of shared dependencies, and manages its own worker pool. In a monorepo with dozens of packages, this multiplies CPU and RAM consumption and often causes CI timeouts. A workspace instead uses one coordinator to schedule tests from all projects across the same worker pool, so resources are allocated once rather than per package.
Configuring a Workspace
Create a vitest.workspace.ts file in the repository root. It acts as a manifest pointing Vitest at your per-project configuration files. Each project keeps its own vitest.config.ts, so environments and setup files can differ per package.
// vitest.workspace.ts (repo root)
import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([
'packages/*/vitest.config.ts',
]);Each matched package then defines its own settings. For example, a backend API package running in Node and a UI package running in jsdom:
// packages/api/vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
setupFiles: ['./test/setup-node.ts'],
},
});
// packages/ui/vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./test/setup-dom.ts'],
},
});Run everything from the root with the same permissions you use for normal test runs (no elevated privileges needed):
# From the repository root
npx vitest runVerifying the Setup
Confirm that Vitest detects each project as a distinct entity before trusting the results:
npx vitest listThe output should show test files grouped per project. If a project is missing, check the glob pattern in vitest.workspace.ts and confirm the package actually contains a vitest.config.ts. To check that project-specific setup files run only where intended, use the verbose reporter and inspect which setup files execute alongside each test file:
npx vitest run --reporter=verboseIf you see the Node setup file executing for jsdom tests, a project is inheriting configuration you did not intend — usually because a shared config is being merged at the root level.
Limits and Common Mistakes
- Memory spikes: All projects run under one coordinator, so concurrent projects with heavy dependencies can exhaust memory. Limit concurrency with
--maxWorkers(ortest.poolOptionsin config) if CI runners run out of RAM. - Global leakage: Globals registered in setup files can cross project boundaries if environments are not explicitly set. Always declare
environmentper project rather than relying on a root default. - Version drift: Keep a single
vitestversion at the root and avoid installing different versions in sub-packages. Mismatches cause confusing dependency resolution and module duplication errors during test discovery. - Root config merging: A root
vitest.config.tsalongside the workspace file can silently merge settings into every project. If projects behave identically when they should not, check for unintended root-leveltestoptions.
A practical health check: run npx vitest list after any config change and diff the project grouping against your package layout. It is the fastest way to catch glob or config-resolution mistakes before they waste a CI run.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.