# How to speed up a slow Stryker run

> Where a small StrykerJS run spends its time, and the ignorePatterns, checker, logLevel and concurrency settings that change it.

Source: https://reapr.dev/guides/speed-up-stryker

A mutation run on a small diff should take seconds of test time. In practice it often takes most of a minute,
and almost none of that is tests. This guide shows where the time went in one measured case and which Stryker
settings changed it.

## The measured case

The numbers come from Reapr's [bench report](https://github.com/reapr-corp/reapr/blob/main/bench/results/2026-11-02-turborepo-monorepo.md):
one package of a pnpm + Turborepo monorepo (~55k files incl. build caches), StrykerJS 9.6.1, Vitest 4, a diff
that produced 3 mutants, and medians of 5 runs per variant on a 22-thread Linux machine. Every run was under
load from other jobs, so treat absolute seconds as inflated. The comparisons between variants are what matter.

## Where the time goes

A median run took 54.0 s. About 85% of it happened before the first test ran:

| Phase | Median | Share |
|---|---|---|
| Start, load the config, walk 55,389 files | 18.0 s | 33% |
| Instrument and preprocess | 4.1 s | 8% |
| Checker, sandbox copy, test runner startup | 24.8 s | 46% |
| Initial test run (83 tests) | 1.4 s | 3% |
| Testing the 3 mutants | 2.7 s | 5% |
| Reporters and temp-dir cleanup | 1.7 s | 3% |

Testing, the initial run plus the mutants, was about 8% of the wall time. Before you tune test speed, look at
startup.

## Keep build caches out of the sandbox

Stryker copies your project into a sandbox directory before it runs anything. This finding applies to
Turborepo repos with a local cache inside the repository (`.turbo/cache`); not every monorepo has one, and a
review of 8 open source Stryker repositories found none. In the measured package, `.turbo/cache` was 53,832 of
the 55,390 files copied (97%) and 1,277.6 MB of the 1.288 GB (99.2%). The config's `ignorePatterns` did not
exclude it.

Adding `.turbo` to `ignorePatterns` took a plain Stryker run from **38.4 s to 12.5 s** (−67%), with identical
results. CPU time fell from 62.8 s to 19.8 s and peak memory from 607 MB to 399 MB.

```js
// stryker.config.mjs
export default {
  // ...your existing config
  ignorePatterns: [".turbo"],
};
```

Stryker 9.6.1 and 10.x always ignore `node_modules`, `.git`, `*.tsbuildinfo`, `/stryker.log`, `.next`, `.nuxt`
and `.svelte-kit`. Anything else in the project directory is copied. Other large directories that your tests never read are candidates too, but do not
ignore files your test runner needs, such as its config or fixtures.

Stryker prints how many files it found at the default log level, in a line like
`ProjectReader Found N of M file(s)`. If M is far above the number of source files you expect, list the largest
directories:

```sh
du -sh .[!.]* * 2>/dev/null | sort -h | tail
```

## Point the TypeScript checker at the files you mutate

The TypeScript checker builds a TypeScript program to discard mutants that do not compile. In the measured
package it cost about 3.2 s and checked nothing. The tsconfig it used had no `include`, so it covered only
three shared Vitest config files. The mutated package was outside that program, and the checker let every
mutant through.

Check what your checker's tsconfig covers:

```sh
npx tsc --listFilesOnly -p tsconfig.json | grep -v node_modules
```

If the files you mutate are not in the list, point `tsconfigFile` at a tsconfig that includes them, or remove
the checker if you do not need compile-error filtering:

```js
export default {
  checkers: ["typescript"],
  tsconfigFile: "tsconfig.json", // must include the files you mutate
};
```

## Use debug logging only to diagnose

`--logLevel debug` is useful to find which phase is slow, but it is not free. In the measured case it added
21 s to the median wall time (59.4 s against 38.4 s at the default level). In one run it held Stryker's event
loop for about 4 s while it printed the list of 55k files.

## Do not raise concurrency for small diffs

With 3 mutants, `--concurrency 8` made the run slower, not faster. Starting four TypeScript checkers in
parallel took 8.9 s against 2.9 s, and mutant testing saved only 1.1 s. The phases added up to 5.7 s more. A
bigger pool pays off when there are many mutants to spread across it.

## About coverageAnalysis

`coverageAnalysis` decides which tests run against each mutant. With `perTest`, the default since Stryker 5,
Stryker runs only the tests that cover the mutant. With `off`, it runs every test for every mutant. Stryker's
docs say it does not change the score. The Vitest runner ignores the setting and always uses `perTest`, so for
Vitest projects there is nothing to tune here. For Jest, Mocha or Jasmine, keep `perTest` unless your tests
depend on each other's order.

## What Reapr does here

Reapr runs your Stryker config as written and does not change any of these settings. In the measured runs,
Reapr's own work outside the Stryker process took 1.7 s. It records the engine time, CPU time and peak memory
of every run in `.git/reapr/runs/<run_id>/run.json`, so you can compare a config change against the runs before
it.

## Frequently asked questions

### Why is Stryker slow even when only a few mutants run?

Most of the time goes to startup, not to tests. In a measured run with 3 mutants, about 85% of the wall time passed before the first test ran: walking the project, instrumenting files, copying the sandbox and starting the checker and test runner.

### What does ignorePatterns do in Stryker?

It lists files and directories that Stryker does not copy into its sandbox. Stryker 9.6.1 and 10.x always ignore node_modules, .git, *.tsbuildinfo, /stryker.log, .next, .nuxt and .svelte-kit. Other caches, such as Turborepo's local cache in .turbo, are copied unless you add them.

### Does coverageAnalysis change the mutation score?

No. Stryker's docs say coverage analysis is a performance optimization that does not influence the score. The default is perTest, and the Vitest runner always uses perTest whatever you set.
