Nx can run a test project in three ways: as one task, as one task per spec file, or as a few groups of specs. Smaller tasks run more in parallel, but each one has to start its test runner first.

I measured this with Cypress, Playwright and Vitest, on one machine and on Nx Agents. I also tested a common claim: atomization shines when some tests are much slower than the others.

Three ways to split a project

Sharding and atomization both add a -ci target. It runs nothing itself, it just depends on its pieces:

One project with 12 specs, three ways

Click a setup to highlight it. Below: the same project with 4 tasks in parallel, as an illustration.

StartupRunning specs
Default: 1 taskdone at 19.01 startupbusy 19.0Sharded: 3 tasksdone at 7.23 startupsbusy 21.0Atomized: 12 tasksdone at 7.712 startupsbusy 30.0

With 4 tasks in parallel, one long task leaves most of the parallelism unused. Sharding runs 3 tasks in parallel for only 3 startups, so it finishes first. Atomization uses all 4, but pays 12 startups and ends later.

The setup

One generated workspace in three sizes, run on one machine with two workloads:

Workspace
12, 50, 100 projects~20 specs per project
Runners
Cypress, Playwright, Vitestcomponent, e2e and unit tests
Quick specs
~1 s per spec~0.2 s for Vitest
Slow projects
1 project in 5 is slow15 s per spec, 3 s for Vitest
Parallelism
--parallel=8up to 8 tasks at once
Machine
Ryzen 9 9950X3D16 cores, 32 threads, 64 GB
Runs
87 valid runsone per configuration

The two workloads answer different questions:

  • Quick specs: every project is about the same size, and every spec takes about a second. It’s a clean, even workload, good for measuring the cost of each split.
  • Slow projects: one project in five gets much slower specs, about 15 s each instead of 1 s. That’s closer to a real workspace, where a few big projects, like a large app’s component tests, take much longer than the rest and decide when the run ends.

Only the plugins in nx.json change between setups:

The Nx plugins without their atomizers. Playwright atomizes unless its ciTargetName is empty.

{
  "plugins": [
    {
      "plugin": "@nx/cypress/plugin",
      "options": { "componentTestingTargetName": "component-test" }
    },
    {
      "plugin": "@nx/playwright/plugin",
      "options": { "targetName": "e2e", "ciTargetName": "" }
    },
    {
      "plugin": "@nx/vitest",
      "options": { "testTargetName": "test", "testMode": "run" }
    }
  ]
}

The workspace generator, the shard plugin and the results are in the repository:

Results

Sharding is the fastest setup in both workloads:

How long the run takes

Every project runs. Hover a point for details.

Default: 1 task per projectSharded: groups of specsAtomized: 1 task per spec
0 s200 s400 s600 s12 projects50 projects100 projects

Switch the workload above:

  • Quick specs: sharding halves the run at 12 projects and ties the default from 50 projects up. Atomization is the slowest from 50 projects up.
  • Slow projects: the default is by far the slowest. Atomization beats it, but sharding is faster still, at every size.

Why splitting helps: unused parallelism at the end

Each run has two parts. The work is how long the run would take if 8 tasks always ran. The idle tail is the extra time when fewer than 8 run, mostly at the end.

Where the run time goes

Work: the run time if 8 tasks always ran. Idle tail: the time lost when fewer ran.

WorkExtra startups from splittingIdle tail
Defaulttail 59 s102 sSharded54 sAtomized+41 s87 s0 s20 s40 s60 s80 s100 s120 s

Splitting shrinks the tail but adds startups:

  • Quick specs: at 12 projects, sharding removes a long tail and adds only a few seconds of work. From 50 projects up, the default’s tail is already short, so there’s little to gain.
  • Slow projects: the default spends a large part of its run in the tail. Both splits remove it, and then atomization’s extra startups make it slower than sharding.

The timeline shows the tail task by task. With slow projects at 12 projects, one big Cypress project runs alone for the last 284 s:

What ran in parallel

One row per parallel task. Empty space is unused parallelism.

App buildCypress CTPlaywrightVitest
Default · 13 tasks · last task ends at 102 sSharded · 29 tasks · last task ends at 53 sAtomized · 231 tasks · last task ends at 87 s0 s20 s40 s60 s80 s100 s120 s

Both splits use nearly all the parallelism. The default leaves much of it unused, most of all with slow projects:

How much of the parallelism was used

On average, how many of the 8 parallel tasks were running, as a share.

Default: 1 task per projectSharded: groups of specsAtomized: 1 task per spec
12 projects42%94%98%50 projects89%97%100%100 projects90%99%99%

The reason is the longest task. In the default, it’s a whole project, and the run can’t end before it does. Sharding cuts it down to a shard, atomization to a single spec:

The longest single task

A run can never finish sooner than its longest task.

Default: 1 task per projectSharded: groups of specsAtomized: 1 task per spec
12 projects95 s20 s7.2 s50 projects147 s23 s7.2 s100 projects130 s23 s7.6 s

Every extra task pays a startup

For the same specs, each setup runs a very different number of tasks:

How many tasks each setup runs

Each app also adds a build task and a server-wait task.

Default: 1 task per projectSharded: groups of specsAtomized: 1 task per spec
12 projects132923150 projects55119897100 projects1102161,694

Each of these tasks starts its runner first. Before its spec runs, a Cypress component-test atom does all this:

Cypress CLI
compiles its config

Vite dev server
pre-bundles deps

Electron launches,
the spec is bundled

the spec runs
(all the report shows)

Startup cost of each extra task

Median across projects, 100-project workspace.

Vitest file0.2 sPlaywright spec0.7 sCypress component spec2.6 s

The startup is the same with quick or slow specs. Cypress pays the most, almost 3 s per task.

Warning ⚠️
These startups belong to this workspace on this machine; yours can be several times longer. Measure them: run one project as a single task, then as atoms, and compare.

These startups explain almost all the gap between atomization and sharding. At 100 projects with quick specs, atomization runs about 1,500 more tasks than sharding. Their startups add about four minutes, and that’s how much later it finishes.

So the runner decides what a split costs:

How long each test runner takes

Each runner ran on its own. Percentages compare with the default.

Default: 1 task per projectSharded: groups of specsAtomized: 1 task per spec
5 Cypress CT projects96 s47 s · −51%70 s · −27%1 Playwright project15 s8.7 s · −40%5.8 s · −60%5 Vitest projects28 s18 s · −34%13 s · −53%All 12 projects102 s54 s · −47%87 s · −15%
  • Cypress: atomization is always slower than sharding, because every Cypress task starts slowly.
  • Playwright and Vitest: their startups are cheap, so atomization can win at 12 projects, where parallelism would otherwise go unused.

Memory stays the same

More, smaller tasks don’t need more memory. With 8 tasks at a time, every setup uses about 8 GB at its busiest:

Memory at the busiest moments

RAM used by all running tasks (99th percentile over the run).

Default: 1 task per projectSharded: groups of specsAtomized: 1 task per spec
12 projects6.5 GB7.1 GB8.2 GB50 projects8.0 GB7.8 GB7.4 GB100 projects7.7 GB8.0 GB7.5 GB

Distributed on Nx Agents

Everything above ran on one machine. Here, Nx Cloud runs the slow-projects workspace with 100 projects across 8 CI agents. Compare the setups with each other, not with the local results.

Workspace
100 projectsslow projects workload
Agents
84 vCPU, ~16 GB each
Scheduling
Nx Clouddistributes the tasks across agents
Runs
3one per setup, no cache

Agent boot is the time from the start of the CI job until the first agent is ready. Test time is everything after that.

Where an Nx Agents run goes

100 projects, 8 agents. Boot installs dependencies and browsers on each agent.

Agent bootTests
Defaulttests 1,296 s1,441 sShardedtests 651 s793 sAtomizedtests 1,002 s1,143 s0 s500 s1,000 s1,500 s
  • Sharded is the fastest: its tests take about 11 minutes.
  • Atomized comes second, about 6 minutes behind sharding.
  • The default is the slowest, because its biggest project runs as one long task.
  • Agent boot is the same for every setup, under 2.5 minutes.

That’s the same order as on one machine.

Total agent time per setup

100 projects, 8 agents. Every agent's whole run, boot included, added up.

Default192 agent-minSharded106 agent-minAtomized152 agent-min

Sharding also uses the least agent time, and the default the most.

Conclusion

With all runners together, sharding was the fastest or tied in every configuration: with quick specs and with slow projects, on one machine and on Nx Agents. It removes the idle tail for only a few startups.

Atomization removes the tail too, but pays a startup for every spec, so its runs end later. The default pays the fewest startups, but its biggest project decides when the run ends.

Speed isn’t the only reason to atomize. On Nx Cloud, atoms also bring per-spec retries and flaky-test detection, which this benchmark didn’t measure.