Skip to main content

tinybench vs mitata vs vitest bench 2026

·PkgPulse Team

tinybench vs mitata vs vitest bench 2026

TL;DR

JavaScript benchmarking has matured beyond Benchmark.js. tinybench (by the Vitest team) is the minimal, no-nonsense option — a tiny library with accurate timing and no dependencies, ideal for library authors and CI benchmarks. mitata is the most statistically rigorous option — it detects V8 deoptimizations, corrects for JIT warmup artifacts, and produces histogram-style output with standard deviation and outlier analysis. vitest bench wraps tinybench with first-class test runner integration, letting you write benchmarks alongside your tests. In 2026, mitata is the choice for serious performance work; tinybench/vitest bench for everyday DX.

Key Takeaways

  • tinybench: ~200 weekly downloads, 800+ GitHub stars, zero dependencies, 1KB bundle — the foundation under vitest bench
  • mitata: 40K+ weekly downloads, 2.3K GitHub stars, V8 deopt detection, histogram output, runs in Node/Bun/Deno
  • vitest bench: ships with Vitest (17M+ weekly downloads), bench() API mirrors test(), HTML report integration, --reporter=verbose
  • Statistical accuracy: mitata corrects for JIT warmup and garbage collection pauses via its own timing harness; tinybench relies on performance.now()
  • V8 deopt detection: mitata flags when V8 deoptimizes your benchmark code — a critical feature most tools miss, leading to misleading results
  • Integration: vitest bench wins for monorepo and test-adjacent benchmarks; mitata wins for standalone microbenchmarks and cross-runtime comparison

Why JavaScript Benchmarking Is Hard

Writing a correct JavaScript benchmark is harder than it looks. The V8 engine applies aggressive optimizations — inline caching, hidden classes, JIT compilation — that depend on code shape and execution history. A benchmark can produce wildly different numbers depending on:

  1. JIT warmup: V8 starts with interpreted execution and promotes hot functions to optimized machine code. Early iterations are slower than warmed-up ones.
  2. Deoptimization: If V8 optimizes a function and then encounters unexpected type changes, it "deoptimizes" — drops back to interpreted mode. A benchmark that triggers deopt is measuring deopt overhead, not your algorithm.
  3. Dead code elimination: V8 can eliminate entire benchmark bodies if it determines the result is never used. Your "benchmark" runs in 0ns because V8 optimized it away.
  4. GC pauses: Garbage collection pauses during your timing window inflate numbers unpredictably.

The best benchmarking libraries account for these issues. The worst ones produce numbers that feel precise but measure the wrong thing.


tinybench

tinybench is a minimalist benchmarking library from the tinylibs collective — the same group behind tinypool and tinyspy. It's written in ~200 lines of TypeScript, has zero runtime dependencies, and is designed to be embedded in other tools (vitest uses it internally).

Basic Usage

import { Bench } from 'tinybench'

const bench = new Bench({ time: 1000 })  // run each task for 1 second

bench
  .add('Array.from', () => {
    Array.from({ length: 1000 }, (_, i) => i)
  })
  .add('new Array + fill', () => {
    new Array(1000).fill(0).map((_, i) => i)
  })
  .add('Array spread + keys', () => {
    [...Array(1000).keys()]
  })

await bench.run()

console.table(
  bench.tasks.map(({ name, result }) => ({
    'Task Name': name,
    'ops/sec': Math.round(result!.hz),
    'Average (ms)': result!.mean.toFixed(4),
    'Margin': ${result!.rme.toFixed(2)}%`,
    Samples: result!.samples.length,
  }))
)

Typical output:

┌─────────────────────┬──────────┬──────────────┬──────────┬─────────┐
│ Task Name           │ ops/sec  │ Average (ms) │ Margin   │ Samples │
├─────────────────────┼──────────┼──────────────┼──────────┼─────────┤
│ Array.from          │ 186,432  │ 0.0054       │ ±0.82%   │ 186     │
│ new Array + fill    │ 241,190  │ 0.0041       │ ±0.61%   │ 241     │
│ Array spread + keys │ 295,847  │ 0.0034       │ ±0.54%   │ 296     │
└─────────────────────┴──────────┴──────────────┴──────────┴─────────┘

Setup and Teardown

tinybench supports per-benchmark setup/teardown and global hooks:

const bench = new Bench({
  time: 500,
  setup: (task, mode) => {
    // Runs before each task iteration (mode: 'run' | 'warmup')
    if (mode === 'run') {
      // Reset shared state
    }
  },
  teardown: (task, mode) => {
    // Cleanup after each iteration
  }
})

bench.add(
  'sort large array',
  () => {
    arr.sort((a, b) => a - b)
  },
  {
    beforeAll() {
      // Runs once before all iterations of this task
      arr = Array.from({ length: 10000 }, () => Math.random())
    },
    beforeEach() {
      // Shuffle before each iteration so we're always sorting unsorted data
      arr = arr.sort(() => Math.random() - 0.5)
    }
  }
)

The beforeEach hook is critical for benchmarks where the initial state matters — without it, sorting an already-sorted array is a best-case scenario that doesn't reflect real workloads.

Warmup Iterations

tinybench runs warmup iterations before timing to allow V8 to JIT-compile the benchmark function:

const bench = new Bench({
  warmupTime: 100,    // ms for warmup phase
  warmupIterations: 5, // minimum warmup iterations
  time: 1000,          // ms for measurement phase
  iterations: 10,      // minimum measurement iterations
})

mitata

mitata takes a fundamentally different approach to benchmarking. Rather than relying solely on performance.now(), mitata instruments the V8 runtime to detect deoptimizations and uses statistical analysis to identify and remove outliers.

Installation

npm install mitata

Basic Usage

import { bench, run, group, baseline } from 'mitata'

bench('Array.from', () => Array.from({ length: 1000 }, (_, i) => i))
bench('new Array + fill', () => new Array(1000).fill(0).map((_, i) => i))
bench('Array spread + keys', () => [...Array(1000).keys()])

await run()

mitata output (ASCII histogram):

clk: ~3.6 GHz
cpu: Apple M3 Pro
runtime: node 22.14.0

benchmark                   avg (min … max) p75   p99    (min … top 1%)
--------------------------- --------------- ----- ------ ---------------
Array.from                    5.27 µs/iter   5.31 µs   5.89 µs  ▄█▆▃▁
new Array + fill              4.11 µs/iter   4.13 µs   4.67 µs  ▃█▄▁
Array spread + keys           3.38 µs/iter   3.39 µs   3.71 µs  ▄█▃▁

The histogram shows the distribution of iteration times — p75 and p99 percentiles reveal tail latency that ops/sec averages hide.

V8 Deoptimization Detection

This is mitata's standout feature:

import { bench, run } from 'mitata'

// This will trigger a V8 deopt because the function receives
// different types across iterations
let input: string | number = 'hello'

bench('polymorphic fn', () => {
  processValue(input)
  input = input === 'hello' ? 42 : 'hello'  // type alternates!
})

await run()
// ⚠️  [polymorphic fn] V8 deoptimization detected
//     This benchmark may not reflect optimized production performance.
//     Consider using a stable type for the input.

Without deopt detection, a benchmark like this produces numbers for the slow, deoptimized path — which V8 would never hit in a real app where types are stable. mitata catches this and warns you.

Groups and Baselines

import { bench, run, group, baseline } from 'mitata'

group('string concatenation', () => {
  baseline('template literal', () => `${a}${b}${c}`)
  bench('+ operator', () => a + b + c)
  bench('Array.join', () => [a, b, c].join(''))
  bench('concat()', () => a.concat(b, c))
})

group('object creation', () => {
  baseline('object literal', () => ({ x: 1, y: 2 }))
  bench('Object.assign', () => Object.assign({}, { x: 1, y: 2 }))
  bench('spread', () => ({ ...{ x: 1, y: 2 } }))
})

await run()

baseline() marks the reference implementation — other benchmarks show their relative speed as a percentage.

Cross-Runtime Support

mitata runs in Node.js, Bun, and Deno without modification:

node bench.ts         # Node.js
bun bench.ts          # Bun (3-5x faster for many ops)
deno run bench.ts     # Deno

This makes it uniquely valuable for library authors who need to verify performance across runtimes.

Async Benchmarks

bench('fetch + parse', async () => {
  const res = await fetch('http://localhost:3000/api/data')
  const json = await res.json()
  return json
})

mitata handles async benchmarks correctly, accounting for Promise overhead and event loop scheduling.


vitest bench

vitest bench integrates benchmarks directly into the Vitest test runner. Benchmarks live alongside tests, share the same config, and appear in the HTML coverage report.

Basic Usage

// math.bench.ts
import { bench, describe, expect } from 'vitest'

describe('array creation', () => {
  bench('Array.from', () => {
    Array.from({ length: 1000 }, (_, i) => i)
  })

  bench('spread + keys', () => {
    [...Array(1000).keys()]
  })

  bench('new Array + map', () => {
    new Array(1000).fill(null).map((_, i) => i)
  })
})

Run with:

vitest bench                      # Run all benchmarks
vitest bench --reporter=verbose   # Detailed per-iteration stats
vitest bench math.bench.ts        # Run specific file

Mixing Tests and Benchmarks

A key advantage: benchmarks and tests share fixtures and utilities:

// parser.bench.ts
import { bench, describe, it, expect } from 'vitest'
import { parseMarkdown } from '../src/parser'

// Tests
it('parses headers', () => {
  const result = parseMarkdown('# Hello')
  expect(result.ast[0].type).toBe('heading')
})

// Benchmarks for the same function
describe('parseMarkdown performance', () => {
  const shortInput = '# Hello\n\nParagraph.'
  const longInput = '# Title\n\n' + 'Content paragraph. '.repeat(500)

  bench('short document', () => {
    parseMarkdown(shortInput)
  })

  bench('long document (10KB)', () => {
    parseMarkdown(longInput)
  })
})

Vitest Bench Config

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    benchmark: {
      include: ['**/*.bench.{ts,js}'],
      exclude: ['**/node_modules/**'],
      reporters: ['verbose'],
      outputJson: 'benchmarks/results.json',  // save results for comparison
    }
  }
})

The outputJson option enables benchmark regression tracking — compare results.json between PRs in CI.

CI Benchmark Regression Detection

// vitest.bench.ts
import { bench, describe } from 'vitest'

describe('critical path', () => {
  bench('render 1000 components', () => {
    renderBatch(components)
  }, {
    time: 2000,
    warmupTime: 500,
    // Fail if ops/sec drops below threshold
    throws: true,
  })
})
# .github/workflows/bench.yml
- name: Run benchmarks
  run: vitest bench --outputJson bench-results.json

- name: Compare with baseline
  uses: CodSpeed-HQ/action@v3
  with:
    token: ${{ secrets.CODSPEED_TOKEN }}

Feature Comparison

Featuretinybenchmitatavitest bench
Bundle size~1KB~15KBpart of Vitest
Weekly downloads~200K~40K17M (Vitest)
V8 deopt detection
Histogram output✅ p75/p99
Async benchmarks
Setup/teardown hooks
Warmup phase
Cross-runtime (Bun/Deno)❌ (Node only)
Test runner integration
HTML report
Baseline comparisons
Statistical rigorBasic (RME)High (histogram)Basic (RME)

When to Choose Each

Choose tinybench if:

  • You're embedding benchmarking in a library or tool (like Vitest does)
  • You need zero-dependency benchmarks with minimal footprint
  • Simple ops/sec output is sufficient for your use case
  • You want a battle-tested primitive to build on

Choose mitata if:

  • Statistical accuracy matters — p75/p99 percentiles, outlier detection
  • You need V8 deoptimization warnings to ensure your benchmark is valid
  • You're doing cross-runtime comparison (Node vs Bun vs Deno)
  • You're benchmarking complex algorithms where JIT behavior matters
  • You're publishing benchmark results in a library README or blog post

Choose vitest bench if:

  • You already use Vitest for testing
  • You want benchmarks co-located with tests, sharing fixtures and mocks
  • You need HTML reports and CI integration out of the box
  • You want to track benchmark regressions across PRs

Common Benchmarking Pitfalls

Understanding these gotchas applies regardless of which tool you choose:

1. Not preventing dead code elimination:

// ❌ V8 may eliminate this entirely
bench('pure computation', () => {
  fibonacci(40)  // result is discarded — V8 might skip it
})

// ✅ Use the return value (mitata does this automatically)
bench('pure computation', () => fibonacci(40))
// mitata captures return value to prevent DCE

2. Using global mutable state:

let counter = 0
// ❌ Hidden side effect — V8 can't optimize this like a pure function
bench('increment', () => {
  counter++
})

3. Benchmarking allocations instead of logic:

// ❌ Measuring JSON.parse + object allocation, not your function
bench('process', () => {
  const data = JSON.parse('{"id":1,"name":"Alice"}')
  processUser(data)
})

// ✅ Pre-allocate outside the hot path
const data = JSON.parse('{"id":1,"name":"Alice"}')
bench('process', () => processUser(data))

4. Ignoring warmup: Most libraries default to 100-500ms of warmup. For complex functions that JIT-compile slowly, you may need more:

// tinybench
const bench = new Bench({ warmupTime: 2000, time: 5000 })

// mitata handles warmup internally based on function complexity

Methodology

  • npm download data from npmjs.com registry API, March 2026
  • tinybench: github.com/tinylibs/tinybench
  • mitata: github.com/nicolo-ribaudo/mitata
  • vitest bench: vitest.dev/guide/features.html#benchmarking
  • Runtime: Node.js 22 on Apple M3 Pro

Compare Vitest, Jest, and other testing tools on PkgPulse.

Related: Vitest vs Jest 2026: Speed, DX, and ESM Support · TanStack Query v5 vs SWR v3 vs RTK Query 2026

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.