Problem
The frontend Vitest suite takes ~51s in CI and ~55s locally (173 files, 2365 tests). Profiling shows the bottleneck is not insufficient parallelism — the suite is already CPU-saturated (~600s of worker CPU spread over 11 cores ≈ the observed wall clock). The bottleneck is that every test file re-imports the whole module graph from scratch.
Vitest's default isolate: true gives each test file a fresh module registry. Measured cost of that:
- Importing
@packmind/ui costs 1.27s (of which @chakra-ui/react alone is 831ms).
- ~150 of the 173 spec files import
@packmind/ui.
- That accounts for roughly 190s of the ~250s total
import time.
In other words, Chakra + the design kit are being evaluated ~150 times per run instead of once per worker.
Measurements
Tight interleaved A/B, same machine (11 cores, macOS, Node 24.18.0, Vitest 4.1.10, Vite 8.1.5). Nx cache was bypassed throughout (--skip-nx-cache, then direct ./node_modules/.bin/vitest invocations).
| Config |
Wall clock |
import |
setup |
| Baseline (current) |
56.9s / 55.0s |
252–262s |
25s |
pool: 'threads' + deps.optimizer.web |
57.7s / 51.7s |
237s |
26s |
isolate: false |
22.9s / 19.4s |
41–60s |
2.3s |
Two things worth calling out:
isolate: false is a ~2.5–3x win. Sharing the module registry per worker collapses ~150 imports of Chakra down to ~11 (one per worker). setup drops 10x for the same reason.
pool: 'threads' and deps.optimizer are worth nothing. An earlier, looser measurement suggested ~10%, but that did not survive a back-to-back A/B on a quiet machine. Ambient machine load dominates at that magnitude — don't bother with these.
Also ruled out: the vite-plugin-checker in apps/frontend/vite.config.ts is not gated on VITEST (unlike the reactRouter() plugin right above it), but it does not actually spawn tsc under Vitest — its checker only activates via configureServer for the dev server. Not a contributor. Gating it would be cosmetic only.
What it costs
Exactly 15 of 173 files leak state under isolate: false. They cluster into three groups:
Shared ApiService / axios module singleton (7 files)
src/services/api/ApiService.test.ts
src/domain/accounts/api/gateways/AuthGatewayApi.test.ts
src/domain/accounts/api/gateways/OrganizationGatewayApi.test.ts
src/domain/accounts/api/gateways/UserGatewayApi.test.ts
src/domain/change-proposals/api/gateways/ChangeProposalsGatewayApi.spec.ts
src/domain/git/api/gateways/GitProviderGatewayApi.spec.ts
src/domain/skills/api/gateways/SkillsGatewayApi.test.ts
Clipboard global (2 files)
src/shared/components/inputs/CopiableTextField.spec.tsx
src/shared/components/inputs/CopiableTextarea.spec.tsx
Component specs (6 files)
src/domain/change-proposals/api/queries/ChangeProposalsQueries.spec.tsx
src/domain/deployments/components/redesign/DeploymentsOverviewRedesign.spec.tsx
src/domain/git/components/ConnectionDrawer/ManageReposPanel.spec.tsx
src/domain/git/components/ManageGitProvider/__tests__/GitProviderAdvancedPanel.spec.tsx
src/domain/marketplaces/components/MarketplaceDetailLayout.spec.tsx
src/domain/spaces/components/SpacesManagementPage/SpacesManagementPage.test.tsx
Suggested fix
Step 1 — bank the win now, without fixing all 15 files. Split into two Vitest projects in apps/frontend/vite.config.ts: the leaky list stays isolated, everything else shares the registry.
const LEAKY_SPECS = [
'src/services/api/ApiService.test.ts',
'src/domain/accounts/api/gateways/*.test.ts',
// ...the 15 files listed above
];
test: {
projects: [
{
extends: true,
test: { name: 'shared', isolate: false, exclude: LEAKY_SPECS },
},
{
extends: true,
test: { name: 'isolated', isolate: true, include: LEAKY_SPECS },
},
],
}
This should take the suite from ~51s to roughly ~20s in CI.
Step 2 — retire the list file by file. The 7 gateway specs share a single root cause (a module-level axios instance in ApiService), so one fix likely clears all of them at once. Each file removed from LEAKY_SPECS moves into the fast project.
Step 3 (optional, smaller) — split the @packmind/ui barrel. packages/ui/src/index.ts re-exports everything including PMCodeMirror, which drags in 6 CodeMirror language grammars (lang-css, lang-json, lang-rust, lang-vue, lang-xml, lang-yaml, legacy-modes). Only 10 frontend components actually use it, but all ~150 spec files pay for it. Moving PMCodeMirror behind a subpath export would trim the non-Chakra portion of the barrel. Note this becomes largely moot once Step 1 lands, since the graph is then evaluated once per worker rather than once per file — worth doing for dev-server and build times, not for tests.
Notes
- Worth doing before reaching for Nx Cloud: this removes the work rather than distributing it.
- Benchmark numbers vary by ±10s with ambient machine load. Any future A/B on this suite must be run back-to-back on a quiet machine, or the result is noise.
🤖 Generated with Claude Code
Problem
The
frontendVitest suite takes ~51s in CI and ~55s locally (173 files, 2365 tests). Profiling shows the bottleneck is not insufficient parallelism — the suite is already CPU-saturated (~600s of worker CPU spread over 11 cores ≈ the observed wall clock). The bottleneck is that every test file re-imports the whole module graph from scratch.Vitest's default
isolate: truegives each test file a fresh module registry. Measured cost of that:@packmind/uicosts 1.27s (of which@chakra-ui/reactalone is 831ms).@packmind/ui.importtime.In other words, Chakra + the design kit are being evaluated ~150 times per run instead of once per worker.
Measurements
Tight interleaved A/B, same machine (11 cores, macOS, Node 24.18.0, Vitest 4.1.10, Vite 8.1.5). Nx cache was bypassed throughout (
--skip-nx-cache, then direct./node_modules/.bin/vitestinvocations).importsetuppool: 'threads'+deps.optimizer.webisolate: falseTwo things worth calling out:
isolate: falseis a ~2.5–3x win. Sharing the module registry per worker collapses ~150 imports of Chakra down to ~11 (one per worker).setupdrops 10x for the same reason.pool: 'threads'anddeps.optimizerare worth nothing. An earlier, looser measurement suggested ~10%, but that did not survive a back-to-back A/B on a quiet machine. Ambient machine load dominates at that magnitude — don't bother with these.Also ruled out: the
vite-plugin-checkerinapps/frontend/vite.config.tsis not gated onVITEST(unlike thereactRouter()plugin right above it), but it does not actually spawntscunder Vitest — its checker only activates viaconfigureServerfor the dev server. Not a contributor. Gating it would be cosmetic only.What it costs
Exactly 15 of 173 files leak state under
isolate: false. They cluster into three groups:Shared
ApiService/ axios module singleton (7 files)src/services/api/ApiService.test.tssrc/domain/accounts/api/gateways/AuthGatewayApi.test.tssrc/domain/accounts/api/gateways/OrganizationGatewayApi.test.tssrc/domain/accounts/api/gateways/UserGatewayApi.test.tssrc/domain/change-proposals/api/gateways/ChangeProposalsGatewayApi.spec.tssrc/domain/git/api/gateways/GitProviderGatewayApi.spec.tssrc/domain/skills/api/gateways/SkillsGatewayApi.test.tsClipboard global (2 files)
src/shared/components/inputs/CopiableTextField.spec.tsxsrc/shared/components/inputs/CopiableTextarea.spec.tsxComponent specs (6 files)
src/domain/change-proposals/api/queries/ChangeProposalsQueries.spec.tsxsrc/domain/deployments/components/redesign/DeploymentsOverviewRedesign.spec.tsxsrc/domain/git/components/ConnectionDrawer/ManageReposPanel.spec.tsxsrc/domain/git/components/ManageGitProvider/__tests__/GitProviderAdvancedPanel.spec.tsxsrc/domain/marketplaces/components/MarketplaceDetailLayout.spec.tsxsrc/domain/spaces/components/SpacesManagementPage/SpacesManagementPage.test.tsxSuggested fix
Step 1 — bank the win now, without fixing all 15 files. Split into two Vitest projects in
apps/frontend/vite.config.ts: the leaky list stays isolated, everything else shares the registry.This should take the suite from ~51s to roughly ~20s in CI.
Step 2 — retire the list file by file. The 7 gateway specs share a single root cause (a module-level axios instance in
ApiService), so one fix likely clears all of them at once. Each file removed fromLEAKY_SPECSmoves into the fast project.Step 3 (optional, smaller) — split the
@packmind/uibarrel.packages/ui/src/index.tsre-exports everything includingPMCodeMirror, which drags in 6 CodeMirror language grammars (lang-css,lang-json,lang-rust,lang-vue,lang-xml,lang-yaml,legacy-modes). Only 10 frontend components actually use it, but all ~150 spec files pay for it. MovingPMCodeMirrorbehind a subpath export would trim the non-Chakra portion of the barrel. Note this becomes largely moot once Step 1 lands, since the graph is then evaluated once per worker rather than once per file — worth doing for dev-server and build times, not for tests.Notes
🤖 Generated with Claude Code