Vitest vs Jest vs node:test in 2026: Benchmarked on a Real TypeScript Project A developer benchmarked Vitest 5.1, Jest 30.5 and Node's built-in node:test against an identical 39-file, 183-test TypeScript suite (387 lines of source, 501 lines of tests) on Node.js 24.22.1, timing cold, warm, single-file, watch and coverage runs as medians of 10 shuffled runs. With default settings Jest plus @swc/jest was fastest on full runs at 0.99 s cold and 0.81 s warm, while Vitest's default forks pool was slowest at 1.59 s cold and 1.51 s warm; disabling isolation flipped the ranking, with node --test --test-isolation=none at 0.52 s cold and Vitest --no-isolate at 0.75 s cold, both beating Jest. The author cautions that the shared VM (load average 3-13 on 8 cores) makes absolute times noisy, so the ranking matters more than the decimals. Cross-posted from my blog. The original, with any updates, lives on fabwaseem.com https://fabwaseem.com/blog/vitest-vs-jest-vs-node-test . I ran the same TypeScript test suite under Vitest 5.1, Jest 30.5 and Node's built-in node:test , and timed cold runs, warm runs, single files, watch mode and coverage. The suite is 39 test files and 183 tests against a small NestJS-style orders service, an HTTP API and an MCP server. The result surprised me: with default settings, Jest with @swc/jest was the fastest full run, and Vitest was the slowest of the three . Change one isolation setting and the order flips. Below are the numbers, the setup for each runner, and the TypeScript and ESM problems I hit along the way. Everything ran on November 3, 2026 on one Linux VM with 8 vCPUs Intel Xeon and 16 GB of RAM, on Node.js 24.22.1: @vitest/coverage-v8 5.1.0 @swc/jest 0.2.39 and @swc/core 1.16.21, and separately with ts-jest 29.4.15 typescript =4.3 <7 , so it doesn't support TypeScript 7.0.2, the current latest on npm. The code under test is real, not generated: money math with cent allocation, slugs, cursor pagination, an LRU cache, a token-bucket rate limiter, retry with backoff, a CSV parser, an event bus, Zod order schemas, a pricing service, an orders service with status transitions, a node:http JSON API, and the MCP server from my MCP testing guide https://fabwaseem.com/blog/test-mcp-server . That's 387 lines of source and 501 lines of tests. The HTTP and MCP tests start real servers on random ports and call them with fetch and the official MCP SDK client. To keep the suite identical, every test file imports describe , it and hooks from t , a one-line shim that each runner maps to its own API, and asserts with node:assert/strict . Fakes are hand-written, so no runner-specific mocking runs inside the benchmark. I compare the mocking APIs separately below. How I measured: a script ran each configuration 10 times in shuffled order. "Cold" means I deleted the runner's caches first jest --clearCache , and node modules/.vite plus node modules/.vitest-cache for Vitest . "Warm" is the next run with caches in place. Times are whole-process wall clock, and I report the median. Every run had to exit 0 with all 183 tests passing. One honest caveat: this VM is shared, and other jobs were running load average between 3 and 13 on 8 cores . Absolute times are noisy, so read the ranking more than the decimals. I ran the main configurations in two separate batches, and Vitest, Jest and node:test each landed within about 0.1 s of their first-batch medians. 39 files, 183 tests, median of 10 runs: | Runner and config | Cold | Warm | |---|---|---| | Vitest 5.1, defaults forks pool, isolated | 1.59 s | 1.51 s | | Vitest, --pool=threads | 1.55 s | 1.42 s | | Vitest, --pool=vmThreads | 0.91 s | 0.91 s | | Vitest, --no-isolate | 0.75 s | 0.68 s | | Vitest, --fsModuleCache | 1.63 s | 1.52 s | | Jest 30.5 + @swc/jest CommonJS output | 0.99 s | 0.81 s | | Jest + @swc/jest , native ESM --experimental-vm-modules | 1.19 s | 1.01 s | | Jest + ts-jest type-checks every file | 4.30 s | 3.13 s | | Jest + ts-jest , isolatedModules: true | 1.44 s | 1.14 s | | node --test process per file | 1.23 s | 1.24 s | | node --test --test-isolation=none | 0.52 s | 0.51 s | The threads, vmThreads and --test-isolation=none rows come from the second batch. In that batch, the Vitest default, Jest + @swc/jest and node --test rows measured 1.52/1.40 s, 0.95/0.80 s and 1.14/1.23 s. What the numbers say: forks pool starts a fresh child process for each test file, and its own summary told me so: "39 workers spawned, ~81ms startup each". node --test also runs each file in its own process. Jest isolates each file in a separate vm context but reuses its 7 worker processes. Turn isolation off and Vitest 0.68 s and node:test 0.51 s both beat Jest. vmThreads is the middle ground for Vitest. vmMemoryLimit and that objects from Node APIs such as fetch 's response.json and structuredClone have a different prototype. That's the same realm problem I hit with Jest below, and this suite only passed under isolatedModules: true it skips the type checker and drops to 1.14 s. Running tsc --noEmit on the whole project separately took about 1.5 s median of 5 , so a separate type-check step is cheaper than type-checking inside Jest. fsModuleCache stable, but on a project this small it made no measurable difference. To see how the gap scales, I copied the test tree three more times: 156 files and 732 tests, same source. Median of 7 runs: | Runner and config | Cold | Warm | |---|---|---| | Vitest, defaults | 5.15 s | 5.03 s | | Vitest, --pool=vmThreads | 2.29 s | 1.99 s | | Vitest, --no-isolate | 1.32 s | 1.08 s | | Jest + @swc/jest | 1.75 s | 1.52 s | | node --test | 4.32 s | 4.26 s | | node --test --test-isolation=none | 1.08 s | 1.10 s | The per-file process cost grows with the file count. Isolated Vitest went from 1.5 s to 5.0 s, while Jest went from 0.8 s to 1.5 s. Without isolation, Vitest and node:test stay around one second. On a busy machine, process startup is exactly the thing that gets slower, so your numbers on a quiet laptop may be closer together, but the direction should hold. Running only test/orders/service.create.test.ts 4 tests , warm, median of 7: | Runner | Time | |---|---| | node --test | 0.18 s | | Jest + @swc/jest | 0.39 s | | Vitest | 0.40 s | | Jest + ts-jest | 1.40 s | Node has nothing to boot except itself. Vitest and Jest each spend about 0.2 s more starting their runner. I started each runner in watch mode, waited for the first run, then edited a source file and timed the rerun median of 5 . money.ts is imported by 3 test files. events.ts is imported by the shared test fixtures, so 18 files depend on it. | Watch command | Edit money.ts | Edit events.ts | |---|---|---| | vitest --watch | 0.23 s, 3 files | 0.97 s, 18 files | | jest --watch needs git | 0.25 s, 3 files | 0.52 s, 18 files | | jest --watchAll | 0.66 s, all 39 files | 0.61 s, all 39 files | | node --test --watch | 0.31 s, 3 files | 0.94 s, 18 files | All three rerun only the affected test files. Vitest follows the module graph, and node:test did the same in my test. Jest's --watch uses git to find changed files, and outside a git repo it refuses to start "--watch is not supported without git/hg, please use --watchAll" . Vitest's interactive watch UI is the nicest of the three filter by name, rerun failed tests, the optional browser UI , but on raw rerun speed they're close. Full suite with coverage on, median of 5 runs after one warm-up: | Runner | Time | Lines reported | |---|---|---| | Vitest, @vitest/coverage-v8 | 1.99 s | 97.38% | | Jest + @swc/jest , default Babel/Istanbul | 0.99 s | 97.81% | | Jest + @swc/jest , --coverageProvider=v8 | 1.66 s | 98.44% | | node --test --experimental-test-coverage | 1.36 s | 100% lines, 95.79% branches | Coverage added about 0.5 s to Vitest and less to Jest with Istanbul. The percentages aren't comparable between tools, because each counts lines, statements and branches differently. Node said 100% of lines while the others found a few uncovered lines in the same code. Pick one tool for your coverage threshold and stay with it. Node's coverage is still behind the --experimental-test-coverage flag on Node 24. js // vitest.config.ts import { defineConfig } from "vitest/config"; export default defineConfig { test: { include: "test/ / .test.ts" , coverage: { provider: "v8", include: "src/ " }, // isolate: false, // 0.68 s instead of 1.51 s, if your tests don't leak state // pool: "vmThreads", // keeps isolation, 0.91 s here }, } ; That's all it needed. ESM, TypeScript, .ts extensions in imports, top-level await and import.meta all worked without extra config. The exception is decorators: a class with a legacy decorator and a constructor parameter property failed with "SyntaxError: Invalid or unexpected token" under my bare config, even with experimentalDecorators in tsconfig.json . Adding unplugin-swc 2.0.0 to the plugins, with experimentalDecorators and emitDecoratorMetadata in tsconfig, fixed it. If you're on NestJS 12, the Vitest config that nest new generates already handles this. I covered that in my NestJS 12 migration guide https://fabwaseem.com/blog/nestjs-12 . // jest.config.cjs / @type {import 'jest' .Config} / module.exports = { testEnvironment: "node", testMatch: "