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

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

Steps

  1. Step 1.

    Print a duration next to every test name.

    npx vitest run --reporter=verbose
    
     RUN  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.

  2. Step 2.

    Write the run as JSON and sort the tests by duration.

    npx vitest run --reporter=json --outputFile=results.json
    
    node -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 rate

    Summing the same field gives 751.9 ms over 9 tests, so one test holds 94% of the time spent inside test bodies.

  3. 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.js
    
     Test 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.

  4. 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 run
    
     RUN  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 Isolate hint reporting 11 workers spawned at around 567 ms of startup each, which is where that share goes.

  5. 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.json
    
    node -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 tests

    The slowest test is 56.4 ms in a 1.09 second run. Removing it saves 5%, and the suite still takes a second.

  6. Step 6.

    Check what --slowTestThreshold adds 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>&1
    
    npx vitest run --reporter=verbose --slowTestThreshold=10 > b.log 2>&1
    
    grep 'upload to settle' a.log b.log
    
    a.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 706ms

    Both 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

Sign: The slowest test gets optimised and the suite takes the same time.Cause: A suite is slow for two different reasons and the reporter separates them. On the 214-test run above, test bodies are 28% of the wall clock, so the entire slowest test is worth 56.4 ms of 1090. The other 72% is import, transform and worker startup.
Sign: slowTestThreshold is set in CI and no slow test is ever flagged.Cause: It controls the colour a duration is printed in, not whether a duration is printed. A redirected log has no colour, so the flag produces a byte-for-byte equivalent line. Use the verbose or json reporter to find slow tests in a log.
Sign: The per-test durations sum to more than the reporter attributes to tests.Cause: Files run in parallel workers, so their durations overlap in wall-clock time. The 214-test run sums to 653.1 ms of test duration while the Duration line gives tests 28% of 1.09 s, about 305 ms. The sum is work done across workers, the percentage is elapsed time, and only the second one shortens when a test gets faster.

Thresholds

445 ms floor, 6 tests Source: Measured with Vitest 5.0.0 on 2026-09-12: two files holding six tests whose durations total under 10 ms complete in 445 ms of wall clock, of which the reporter attributes 8% to tests.

What to check next

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.

intermediate8 minpublished updated Maks Verny