Vitest vs. Jest: 11 Lines of Config and 331 Tests in 2.48s
Measured on this repo: 31 files, 331 tests, 2.48s — and the `environment 3ms` line that explains most of the Vitest-versus-Jest gap.
This storefront's entire test configuration is eleven lines, and there is no babel.config.js, no jest.config.js and no ts-jest anywhere in the repository. That is the practical difference between Vitest and Jest for a TypeScript project in 2026 — not the benchmark numbers, which both sides can arrange to win, but how much configuration sits between a fresh checkout and a passing suite.
The short answer
Vitest is a Vite-native runner: ESM and TypeScript work with no transform layer, and it reuses your existing Vite resolution. Jest is the older, larger ecosystem and needs a transform step (Babel or ts-jest) to read TypeScript. If your project already builds with Vite or Next.js, Vitest removes config rather than adding it.
The whole configuration
Here is every line of test configuration in this repo:
// vitest.config.ts
import { defineConfig } from "vitest/config";
import path from "node:path";
export default defineConfig({
resolve: {
alias: { "@": path.resolve(__dirname, "src") },
},
test: {
include: ["src/**/__tests__/**/*.test.ts"],
},
});
Two settings. The alias mirrors the one in tsconfig.json so @/lib/... resolves the same way in tests as in the app. The include glob is .test.ts — deliberately not .test.tsx, which is a decision this post gets to later.
What is absent is the interesting part. No transform block. No preset. No moduleNameMapper for the path alias, no extensionsToTreatAsEsm, no testEnvironment. On a Jest setup those are the lines you write first and debug longest, because Jest's runtime is CommonJS-first and TypeScript is not something it reads natively.
What it costs to run
Measured on this checkout, not quoted from a vendor page:
Test Files 31 passed (31)
Tests 331 passed (331)
Duration 2.48s (transform 1.40s, setup 0ms, import 2.17s,
tests 801ms, environment 3ms)
Three of those numbers say something a benchmark headline cannot.
tests 801ms — the assertions themselves are under a second. Most of the 2.48s is getting the modules loaded, which is true of any runner and is where Vite's transform pipeline earns its keep.
setup 0ms — there are no setup files. Nothing to run before the suite, because nothing needs installing into a global environment.
environment 3ms — this is the one to look at. Jest's defaults push people toward testEnvironment: "jsdom", and standing up a jsdom document per test file is a real, repeated cost. Here it is three milliseconds across 31 files, because the environment is plain Node.
Why environment 3ms and not 300ms
The suite has no jsdom and no @testing-library — not because they are bad, but because of what these 331 tests are for. They assert data invariants (every catalog product resolves to real screenshot files; every sitemap date is a declared day rather than a build-clock reading) and security boundaries (download authorization, redemption IDOR, webhook signature verification). None of that needs a DOM.
Exactly one suite touches browser globals, and it declines to pull in jsdom for them:
// src/lib/__tests__/wishlist.test.ts
/**
* `readWishlist`/`toggleWishlist` touch `window`, so the suite runs against a
* minimal in-memory stand-in rather than pulling in jsdom for three globals.
*/
function installBrowserGlobals() {
const store = new Map<string, string>();
const win = {
localStorage: {
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => void store.set(k, v),
removeItem: (k: string) => void store.delete(k),
},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
};
(globalThis as { window?: unknown }).window = win;
(globalThis as { Event?: unknown }).Event ??= class {
constructor(public type: string) {}
};
return store;
}
Three globals, hand-written, in the space a testEnvironment line would have occupied. This is the choice the include: ["**/*.test.ts"] glob encodes: .tsx is excluded so that rendering components in a test is a decision someone has to make deliberately, not the default that quietly drags in a DOM implementation and a rendering library.
Vitest supports jsdom and happy-dom perfectly well — environment: "jsdom" is one line. The point is that the runner did not assume it, so the 3ms is what not opting in actually costs.
The comparison that matters
| Vitest | Jest | |
|---|---|---|
| TypeScript | Native, via Vite's transform | Needs Babel or ts-jest |
| ESM | Native from the first line | Supported, historically with flags and friction |
| Config for this repo | 11 lines, 2 settings | A preset, a transform, a moduleNameMapper for the alias |
| Path aliases | Reuses Vite resolve.alias | Declared again in moduleNameMapper |
| Mocking API | vi.* | jest.* |
| Default environment | Node | Commonly configured to jsdom |
| Ecosystem age | Newer, smaller | Larger, more Stack Overflow answers |
| API shape | Near drop-in for Jest's | The API Vitest copied |
The last two rows are the honest case for Jest and they are not nothing. If you hit an obscure problem, there are ten years of answers about Jest and considerably fewer about Vitest. And Jest works with almost any JavaScript project, including ones with no Vite anywhere — a Vite-native runner is a weaker fit when there is no Vite in the build.
The case for Vitest is narrower and, for this project, decisive: the config that does not exist cannot break.
A test worth stealing, whichever runner you pick
The runner matters less than what you point it at. This suite's most valuable assertion has nothing to do with components:
it("EVERY date in the sitemap is a declared day, not a clock reading", () => {
for (const entry of entries) {
const iso = new Date(entry.lastModified!).toISOString();
expect(iso.endsWith("T00:00:00.000Z"), `${entry.url} → ${iso}`).toBe(true);
}
});
A new Date() left in sitemap.ts tells Google that a hundred URLs changed on every deploy — a checkable false claim that costs crawl scheduling. It is invisible in review, it breaks nothing at runtime, and no amount of component testing finds it. A midnight-UTC check does, in four lines, because a declared date is midnight UTC and a build-clock reading never is.
It also caught a live regression here. Product and hub URLs were covered from the start; the eleven static routes were not, and kept shipping new Date() — so /, /blog, /terms and eight others claimed to change on every deploy while the file's own doc comment explained why that costs crawl scheduling. The exemption was the bug. Widening the sweep to every entry is what closed it, because "which tier is exempt" was exactly the question that let the regression live.
That is the argument for a fast, DOM-free suite: when tests cost 801ms, you write the paranoid ones.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Cannot find module '@/lib/...' in tests | Path alias resolved by TS but not by the runner | Mirror it in resolve.alias (Vitest) or moduleNameMapper (Jest) |
vitest: not found on a fresh clone | Dependencies not installed | npm install — the binary is local, not global |
document is not defined | Default environment is Node | Either set environment: "jsdom" or stub the two or three globals you need |
| Jest chokes on an ESM-only dependency | CommonJS-first runtime | transformIgnorePatterns surgery, or move to a native-ESM runner |
| Suite passes locally, fails in CI | Time zone or clock assumptions in the test | Assert on declared values, not on new Date() |
| Tests slow with no obvious cause | jsdom stood up per file | Check the environment line in the run summary |
Frequently asked questions
Is Vitest actually faster than Jest? Both can be fast, and any single benchmark is arrangeable. What this repo can show is its own run: 331 tests in 2.48s with no transform config in the middle and 3ms of environment setup. How much of that gap is the runner versus the absence of jsdom is exactly the point — a large share of "Jest is slow" in practice is a jsdom default nobody revisited.
Can I migrate from Jest without rewriting tests?
Mostly. Vitest was designed as a near drop-in for the Jest API; the routine edits are jest.* → vi.* and deleting config you no longer need. The globals behave the same.
Should I use Vitest with Next.js?
It works — this repo is Next 16 and runs Vitest on npm test in CI alongside lint, format and build. Just be clear about what you are testing: pure logic, data invariants and server-side boundaries run beautifully in plain Node. Component rendering is a separate decision with a separate cost.
What about end-to-end tests? Different tool, different job. Vitest covers units and invariants; browser-level flows belong in Playwright, and we compared those two directly in Playwright vs. Vitest. For what this project's 31 files actually assert and what that leaves uncovered, see how this storefront tests a Next.js app.
Templates in this post
ASoc Nexus is a SaaS app landing page template. ASoc Nimbus is a cloud SaaS landing page template. ASoc Nova is a crypto trading landing page template. All three ship as Next.js + Tailwind projects with the same lint, format and type-check discipline described above.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
