Skip to main content
ASoc
Comparison

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.

The ASoc Team8 min read

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

VitestJest
TypeScriptNative, via Vite's transformNeeds Babel or ts-jest
ESMNative from the first lineSupported, historically with flags and friction
Config for this repo11 lines, 2 settingsA preset, a transform, a moduleNameMapper for the alias
Path aliasesReuses Vite resolve.aliasDeclared again in moduleNameMapper
Mocking APIvi.*jest.*
Default environmentNodeCommonly configured to jsdom
Ecosystem ageNewer, smallerLarger, more Stack Overflow answers
API shapeNear drop-in for Jest'sThe 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

SymptomCauseFix
Cannot find module '@/lib/...' in testsPath alias resolved by TS but not by the runnerMirror it in resolve.alias (Vitest) or moduleNameMapper (Jest)
vitest: not found on a fresh cloneDependencies not installednpm install — the binary is local, not global
document is not definedDefault environment is NodeEither set environment: "jsdom" or stub the two or three globals you need
Jest chokes on an ESM-only dependencyCommonJS-first runtimetransformIgnorePatterns surgery, or move to a native-ESM runner
Suite passes locally, fails in CITime zone or clock assumptions in the testAssert on declared values, not on new Date()
Tests slow with no obvious causejsdom stood up per fileCheck 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.

Keep reading

Comparison10 min read

Webflow vs Next.js for a Landing Page: How to Actually Decide

The launch-speed argument mostly disappears once you compare template to template. What is left is who edits the copy, what each locks in, and three cases where Webflow wins.

Read more
Comparison12 min read

WordPress vs Next.js for a Marketing Site: The Honest Comparison

Most comparisons pit a WordPress theme against a from-scratch Next.js build. Compare theme to template instead and the real trade turns out to be who edits the copy.

Read more
Comparison10 min read

Angular vs. Svelte: 7 of 24 Client Components Need No State At All

DI-injected signals versus a build-time compiler. Both assume a component needs a reactive primitive — 7 of this codebase's 24 Client Components prove a third of the time, it doesn't.

Read more