How to check which tests are slowest
Run npx vitest run --reporter=verbose and read the millisecond figure after each test name. For a sorted list, write the run as JSON with --reporter=json --outputFile=results.json and sort by duration. On the project below the slowest test is 708.5 ms out of 751.9 ms across nine tests.
Why check this
A tester runs this when the suite has grown long enough that people stop running it before pushing. At that point the pipeline stops catching things early, so the minutes are worth attributing.
The failure it prevents is a week spent optimising the wrong thing. The obvious move is to find the slowest test and speed it up, and on a suite of any size the slowest test is often not why the suite is slow. On the 214-test suite measured in step 4, test bodies account for 28% of a 1.09 second run, and the slowest single test is 56.4 ms. Deleting it saves 5% of the wall clock.
What this check settles: which tests carry the time, and how much of the wall clock belongs to tests at all. Both numbers come out of the same run.
Prerequisites
- Node 22 and npm. Figures here came from Node 22.23.2 with Vitest 5.0.0 on 2026-09-12.
- A project with one clearly slow test. Build this one in an empty directory outside your repository. See the Vitest reporters documentation.
mkdir covdemo && cd covdemo
npm init -y > /dev/null
npm pkg set type=module
npm install -D vitest@5
mkdir src test
cat > src/cart.js <<'EOF'
export function addItem(cart, item) {
const next = cart.slice();
const at = next.findIndex((i) => i.sku === item.sku);
if (at === -1) {
next.push({ ...item });
} else {
next[at] = { ...next[at], qty: next[at].qty + item.qty };
}
return next;
}
export function removeItem(cart, sku) {
return cart.filter((i) => i.sku !== sku);
}
export function itemCount(cart) {
return cart.reduce((n, i) => n + i.qty, 0);
}
export function subtotal(cart) {
return cart.reduce((n, i) => n + i.qty * i.price, 0);
}
EOF
cat > src/discount.js <<'EOF'
export function priceFor(order) {
const rate = order.tier === 'vip' ? 0.2 : 0.1;
const shipping = order.shipping ?? 5;
const net = order.total * (1 - rate);
return Math.round(net + shipping);
}
EOF
cat > src/export-job.js <<'EOF'
export function makeRows(n) {
const rows = [];
for (let i = 0; i < n; i += 1) rows.push({ sku: 'A' + i, qty: (i % 7) + 1, price: (i % 13) + 0.5 });
return rows;
}
export async function exportRows(rows) {
const out = [];
for (const row of rows) {
out.push(`${row.sku},${row.qty},${(row.qty * row.price).toFixed(2)}`);
if (out.length % 2000 === 0) await new Promise((r) => setImmediate(r));
}
return out.join('\n');
}
EOF
cat > test/cart.test.js <<'EOF'
import { describe, it, expect } from 'vitest';
import { addItem, removeItem, itemCount, subtotal } from '../src/cart.js';
describe('cart', () => {
it('adds a new sku', () => {
expect(addItem([], { sku: 'A1', qty: 1, price: 10 })).toEqual([{ sku: 'A1', qty: 1, price: 10 }]);
});
it('merges an existing sku', () => {
const cart = [{ sku: 'A1', qty: 1, price: 10 }];
expect(addItem(cart, { sku: 'A1', qty: 2, price: 10 })[0].qty).toBe(3);
});
it('removes a sku', () => {
expect(removeItem([{ sku: 'A1', qty: 1, price: 10 }], 'A1')).toEqual([]);
});
it('counts items', () => {
expect(itemCount([{ sku: 'A1', qty: 2, price: 10 }])).toBe(2);
});
it('sums the subtotal', () => {
expect(subtotal([{ sku: 'A1', qty: 2, price: 10 }])).toBe(20);
});
});
EOF
cat > test/discount.test.js <<'EOF'
import { describe, it, expect } from 'vitest';
import { priceFor } from '../src/discount.js';
describe('priceFor', () => {
it('applies the standard rate', () => {
expect(priceFor({ tier: 'standard', total: 100, shipping: 5 })).toBe(95);
});
});
EOF
cat > test/export-job.test.js <<'EOF'
import { describe, it, expect } from 'vitest';
import { makeRows, exportRows } from '../src/export-job.js';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
describe('exportRows', () => {
it('writes one line per row', async () => {
const csv = await exportRows(makeRows(3));
expect(csv.split('\n')).toHaveLength(3);
});
it('finishes the export before the deadline', async () => {
const rows = makeRows(20000);
const deadline = sleep(25).then(() => 'deadline');
const result = await Promise.race([exportRows(rows), deadline]);
expect(result).not.toBe('deadline');
});
it('waits for the upload to settle', async () => {
await sleep(700);
expect(await exportRows([])).toBe('');
});
});
EOF
- One machine, one run. Durations move by a few milliseconds between runs and by much more between machines, so compare figures from the same sitting.
Steps
- Step 1.
Print a duration next to every test name.
npx vitest run --reporter=verboseRUN v5.0.0 C:/…/covdemo ✓ test/cart.test.js > cart > adds a new sku 4ms ✓ test/cart.test.js > cart > merges an existing sku 1ms ✓ test/cart.test.js > cart > removes a sku 0ms ✓ test/cart.test.js > cart > counts items 1ms ✓ test/cart.test.js > cart > sums the subtotal 0ms ✓ test/discount.test.js > priceFor > applies the standard rate 3ms ✓ test/export-job.test.js > exportRows > writes one line per row 4ms ✓ test/export-job.test.js > exportRows > finishes the export before the deadline 28ms ✓ test/export-job.test.js > exportRows > waits for the upload to settle 704ms Test Files 3 passed (3) Tests 9 passed (9) Duration 1.14s (tests 72%, import 13%, transform 12%, worker 2%)Nine lines is readable. Nine hundred is not, which is what step 2 is for.
- Step 2.
Write the run as JSON and sort the tests by duration.
npx vitest run --reporter=json --outputFile=results.jsonnode -e "const r=require('./results.json');r.testResults.flatMap(f=>f.assertionResults.map(a=>({d:a.duration,n:a.fullName}))).sort((x,y)=>y.d-x.d).slice(0,5).forEach(t=>console.log(t.d.toFixed(1).padStart(8)+' ms '+t.n))"708.5 ms exportRows waits for the upload to settle 30.5 ms exportRows finishes the export before the deadline 4.2 ms exportRows writes one line per row 3.6 ms cart adds a new sku 3.4 ms priceFor applies the standard rateSumming the same field gives 751.9 ms over 9 tests, so one test holds 94% of the time spent inside test bodies.
- Step 3.
Find the floor by running only the fast files, and compare it with the wall clock above.
npx vitest run test/cart.test.js test/discount.test.jsTest Files 2 passed (2) Tests 6 passed (6) Duration 445ms (transform 51%, import 30%, worker 10%, tests 8%, environment 1%)Six tests whose durations total under 10 ms take 445 ms of wall clock. That number is the price of starting the runner, and no test change lowers it.
- Step 4.
Read the same breakdown on a suite large enough for the split to matter. The run below is the h2check repository itself: 11 files, 214 tests, and no deliberately slow test in it.
npx vitest runRUN v5.0.0 D:/how2check Test Files 11 passed (11) Tests 214 passed (214) Duration 1.09s (transform 36%, import 32%, tests 28%, worker 3%)Transform and import take 68% between them, test bodies 28%. A slower run of the same suite earlier the same day added an
Isolatehint reporting 11 workers spawned at around 567 ms of startup each, which is where that share goes. - Step 5.
Sort the same suite by duration and add the total, to see what its slowest test is worth.
npx vitest run --reporter=json --outputFile=results.jsonnode -e "const r=require('./results.json');const a=r.testResults.flatMap(f=>f.assertionResults);a.map(x=>({d:x.duration,n:x.fullName})).sort((x,y)=>y.d-x.d).slice(0,5).forEach(t=>console.log(t.d.toFixed(1).padStart(8)+' ms '+t.n));console.log('sum '+a.reduce((s,x)=>s+x.duration,0).toFixed(1)+' ms over '+r.numTotalTests+' tests')"56.4 ms cron checker parses a standard five-field expression 53.6 ms cron checker applies a named timezone 47.7 ms json schema checker passes a document that satisfies the schema 31.7 ms zone arithmetic computes a whole-hour offset on both sides of a DST change 27.4 ms SHA-256 against FIPS 180-4 hashes one million "a" characters sum 653.1 ms over 214 testsThe slowest test is 56.4 ms in a 1.09 second run. Removing it saves 5%, and the suite still takes a second.
- Step 6.
Check what
--slowTestThresholdadds to a log before relying on it in CI. Capture one run at the default threshold and one at 10 ms.npx vitest run --reporter=verbose > a.log 2>&1npx vitest run --reporter=verbose --slowTestThreshold=10 > b.log 2>&1grep 'upload to settle' a.log b.loga.log: ✓ test/export-job.test.js > exportRows > waits for the upload to settle 702ms b.log: ✓ test/export-job.test.js > exportRows > waits for the upload to settle 706msBoth lines have the same shape, and 702 against 706 is run-to-run variation on one 700 ms sleep. The threshold changes terminal colour, and colour does not survive a redirect.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| One test far above the rest | A sleep, a real network call or a heavy fixture | Read that test first. It is usually waiting, not computing |
| Duration with tests under a third | Most of the run is startup, import and transform | Reduce file count or worker isolation, not test bodies |
| A high transform share | Source is being compiled on every run | Check the cache, and whether a config change invalidated it |
| A per-file startup figure in the Isolate hint | Each test file is paying for its own worker | Try isolate: false and rerun both numbers before keeping it |
| The duration sum above the tests share of Duration | Files ran in parallel, so the times overlap | Treat the sum as work done, not as elapsed time |
| Durations that move on every run | Normal variation, or a timing-sensitive test | Repeat the run before acting on a difference under 10% |
Common mistakes
Thresholds
What to check next
- How to detect flaky tests: the slowest tests are usually the ones with a timer in them, and timers are what flake.
- How to check test coverage: instrumentation lengthens the run, so measure the suite before and after turning it on.
- How to check branch coverage: closing branch gaps adds cases, which is the usual reason a suite grows.
- How to check code coverage threshold: the gate that decides whether the longer run was worth it.
- Pull request checklist: the checks worth keeping fast enough to run every time.
FAQ
How do I see how long each test took in Vitest?
--reporter=verbose prints a duration after every test name. The default reporter prints one line per file and a total, so a single slow test inside a file is invisible until you switch reporter.
How do I sort tests by duration?
Write the run with --reporter=json --outputFile=results.json, then sort assertionResults by the duration field. Step 2 does it in one node -e line, which works the same on a suite of nine tests and a suite of nine hundred.
Is the slowest test the reason my suite is slow?
Check the Duration line before assuming so. It splits the wall clock into tests, import, transform and worker time. When tests are a quarter of the total, the slowest test is not the problem.
What is a reasonable duration for a unit test?
Anything that waits is the wrong shape, whatever the number. The 704 ms test above sleeps and computes nothing. Fake timers usually make such a test instant, which is a larger win than tuning a fast one.
Why did the same test report a different duration on two runs?
Timing varies with machine load, garbage collection and compilation state. Step 6 measured 702 ms and 706 ms for one test that sleeps a fixed 700 ms. Treat a difference under 10% as noise.
Verified
Verified by Maks Vernynode 22.23.2vitest 5.0.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate8 minpublished updated Maks Verny