Vitest 4 — Testing Framework 28x Faster Than Jest for Vue and Vite

Posted on: 4/26/2026 1:13:24 PM

28x Faster than Jest (watch mode)
5.6x Faster cold start
57% Less memory usage
4.0 Latest stable version

1. What is Vitest and Why It Matters

Vitest is a next-generation testing framework built on top of Vite — the build tool dominating the frontend ecosystem. Instead of wrestling with complex configurations involving babel, ts-jest, and numerous plugins as with Jest, Vitest directly leverages Vite's transform pipeline: TypeScript, JSX, and CSS Modules all work out of the box with zero configuration.

With version 4.0 released in 2026, Vitest marks a major milestone as Browser Mode officially becomes stable, Visual Regression Testing is built in, and Playwright Traces make debugging failed tests more visual than ever.

Why switch from Jest to Vitest?

Jest was designed for the CommonJS era. Even though Jest 30 has improved ESM support, configuration remains complex — you need ts-jest or babel-jest for TypeScript, moduleNameMapper for path aliases, and separate CSS mocking. Vitest inherits the entire config from vite.config.ts, so all aliases, plugins, and transforms work consistently between the dev server and test runner.

2. Vitest Architecture Internals

Vitest isn't simply "Jest running on Vite." Its architecture is redesigned from scratch to fully leverage native ESM and Hot Module Replacement:

graph TB
    A["vite.config.ts
Shared Config"] --> B["Vite Dev Server
Transform Pipeline"] B --> C["Vitest Runner
Test Scheduler"] C --> D["Worker Pool
tinypool threads"] C --> E["Browser Mode
Playwright / WebdriverIO"] D --> F["Node.js Tests
Unit & Integration"] E --> G["Real Browser Tests
Component & E2E"] B --> H["HMR Watcher
File Change Detection"] H --> C style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style C fill:#2c3e50,stroke:#fff,color:#fff style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style F fill:#f8f9fa,stroke:#e0e0e0,color:#555 style G fill:#f8f9fa,stroke:#e0e0e0,color:#555 style H fill:#16213e,stroke:#fff,color:#fff

Vitest architecture: leveraging Vite's transform pipeline and HMR for blazing fast watch mode

When you change a file, Vite HMR pinpoints exactly which modules are affected. Vitest only re-runs tests related to those modules — no need to re-parse the entire dependency graph like Jest. This is why watch mode is 28x faster.

3. Vitest 4.0 — New Features

3.1. Browser Mode is now stable

After multiple experimental releases, Browser Mode in Vitest 4.0 has exited experimental status. Instead of testing components in jsdom (a simulated DOM environment), you run tests in a real browser via Playwright or WebdriverIO:

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

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      instances: [
        { browser: 'chromium' },
        { browser: 'firefox' },
        { browser: 'webkit' }
      ]
    }
  }
})

Providers are now separate packages:

  • @vitest/browser-playwright — for Playwright
  • @vitest/browser-webdriverio — for WebdriverIO
  • @vitest/browser-preview — for preview mode

3.2. Built-in Visual Regression Testing

Vitest 4.0 introduces the toMatchScreenshot assertion — enabling screenshot comparison directly in tests without external libraries:

import { test, expect } from 'vitest'
import { page } from 'vitest/browser'

test('hero section renders correctly', async () => {
  await page.goto('/landing')

  await expect(
    page.getByTestId('hero-section')
  ).toMatchScreenshot('hero-section')
})

test('element is visible in viewport', async () => {
  await expect(
    page.getByRole('banner')
  ).toBeInViewport({ ratio: 0.8 })
})

Practical benefit

Visual Regression Testing catches UI changes that unit tests can't: fonts being swapped, spacing off by 2px, colors wrong due to CSS specificity. Before Vitest 4, you needed separate tools like Percy, Chromatic, or complex jest-image-snapshot configurations.

3.3. Playwright Traces

When browser tests fail, debugging is often difficult because you don't know the DOM state at the moment of failure. Vitest 4.0 integrates Playwright Traces — recording the entire interaction timeline:

# Enable traces on first retry
npx vitest --browser.trace=on-first-retry

# Enable traces on all retries
npx vitest --browser.trace=on-all-retries

# Only keep traces when tests fail
npx vitest --browser.trace=retain-on-failure

Traces appear as annotations in reporters, and you can open them in Playwright Trace Viewer to inspect each step: clicks, navigation, network requests, DOM snapshots.

3.4. Schema Matching with Zod, Valibot, ArkType

The new expect.schemaMatching feature validates returned data against Standard Schema v1:

import { z } from 'zod'
import { expect, test } from 'vitest'

const UserSchema = z.object({
  id: z.number().positive(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'editor'])
})

test('API returns valid user', async () => {
  const response = await fetch('/api/user/1')
  const user = await response.json()

  expect(user).toEqual({
    id: expect.any(Number),
    email: expect.schemaMatching(z.string().email()),
    role: expect.schemaMatching(z.enum(['admin', 'user', 'editor']))
  })
})

3.5. Type-Aware Fixtures

Vitest 4.0 extends test fixtures with type-safe lifecycle hooks:

import { test as base } from 'vitest'

const test = base.extend<{ todos: string[] }>({
  todos: async ({}, use) => {
    const list: string[] = []
    await use(list)
    // cleanup after test
  }
})

// beforeEach has full type inference
test.beforeEach(({ todos }) => {
  todos.push('initial item')
})

test('add todo', ({ todos }) => {
  todos.push('new item')
  expect(todos).toHaveLength(2)
})

4. Benchmark: Vitest vs Jest — Real Numbers

Benchmarks performed on the same 500 test files suite, running Node.js 22, 16GB RAM machine:

Metric Vitest 3/4 Jest 30 Difference
Cold start (500 files) ~38 seconds ~214 seconds 5.6x faster
Watch mode re-run ~0.3 seconds ~8.4 seconds 28x faster
Peak memory ~400 MB ~930 MB 57% less
Native ESM Yes Partial support
TypeScript Native via Vite Requires ts-jest/babel Zero config
Browser testing Playwright/WebdriverIO jsdom only Real browser
CSS Modules Automatic via Vite Requires moduleNameMapper Zero config
graph LR
    subgraph Jest["Jest 30"]
        J1["Source File"] --> J2["babel-jest
Transform"] J2 --> J3["ts-jest
TypeScript"] J3 --> J4["moduleNameMapper
Aliases"] J4 --> J5["jsdom
DOM Simulation"] end subgraph Vitest["Vitest 4"] V1["Source File"] --> V2["Vite Pipeline
TS + JSX + CSS"] V2 --> V3["Real Browser
or Node.js"] end style J1 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J2 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J3 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J4 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style J5 fill:#f8f9fa,stroke:#e0e0e0,color:#555 style V1 fill:#e94560,stroke:#fff,color:#fff style V2 fill:#e94560,stroke:#fff,color:#fff style V3 fill:#e94560,stroke:#fff,color:#fff

Transform pipeline: Jest needs multiple plugin layers, Vitest uses Vite directly

5. Integrating Vitest with Vue 3

5.1. Basic setup

# Installation
npm install -D vitest @vue/test-utils @vitejs/plugin-vue happy-dom
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',
    globals: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov']
    }
  }
})

5.2. Component Testing

// TodoList.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import TodoList from './TodoList.vue'

describe('TodoList', () => {
  it('renders todos from props', () => {
    const wrapper = mount(TodoList, {
      props: {
        items: ['Learn Vitest', 'Write tests']
      }
    })

    expect(wrapper.findAll('li')).toHaveLength(2)
    expect(wrapper.text()).toContain('Learn Vitest')
  })

  it('emits add event on form submit', async () => {
    const wrapper = mount(TodoList, {
      props: { items: [] }
    })

    await wrapper.find('input').setValue('New todo')
    await wrapper.find('form').trigger('submit')

    expect(wrapper.emitted('add')).toHaveLength(1)
    expect(wrapper.emitted('add')![0]).toEqual(['New todo'])
  })
})

5.3. Testing Composables

// useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('increments count', () => {
    const { count, increment } = useCounter()

    expect(count.value).toBe(0)
    increment()
    expect(count.value).toBe(1)
  })

  it('respects max limit', () => {
    const { count, increment } = useCounter({ max: 2 })

    increment()
    increment()
    increment() // should not exceed max
    expect(count.value).toBe(2)
  })
})

5.4. Testing Pinia Stores

// userStore.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useUserStore } from './userStore'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('fetches user profile', async () => {
    const store = useUserStore()

    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      new Response(JSON.stringify({
        name: 'Anh Tu',
        role: 'admin'
      }))
    )

    await store.fetchProfile()

    expect(store.user?.name).toBe('Anh Tu')
    expect(store.isAdmin).toBe(true)
  })
})

6. Browser Mode — Testing Components in Real Browsers

The biggest difference between jsdom and Browser Mode: jsdom has no layout engine. APIs like getBoundingClientRect(), IntersectionObserver, ResizeObserver, and CSS animations don't work correctly in jsdom. Browser Mode completely solves this.

// vitest.config.ts — Browser Mode for Vue
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    browser: {
      enabled: true,
      instances: [
        { browser: 'chromium', provider: 'playwright' }
      ]
    }
  }
})
// Dialog.browser.test.ts
import { test, expect } from 'vitest'
import { page } from 'vitest/browser'
import { render } from 'vitest-browser-vue'
import Dialog from './Dialog.vue'

test('dialog has correct position and animation', async () => {
  const screen = render(Dialog, {
    props: { open: true, title: 'Confirm' }
  })

  const dialog = screen.getByRole('dialog')

  // Real browser APIs work!
  await expect(dialog).toBeInViewport({ ratio: 1.0 })

  // Visual regression test
  await expect(dialog).toMatchScreenshot('dialog-open')
})

Important note

Browser Mode requires Playwright installation: npx playwright install. In CI, add a browser installation step to your pipeline. Browser tests are slower than pure Node.js unit tests, so only use them for components that genuinely need a real DOM (animations, layout, visual regression).

7. Migrating from Jest to Vitest

Vitest's API is designed to be nearly 100% compatible with Jest. Migration typically takes just a few steps:

Step 1: Install
npm install -D vitest and remove jest, ts-jest, babel-jest, @types/jest from dependencies.
Step 2: Config
Delete jest.config.js. Add a test block to vite.config.ts or create vitest.config.ts. No need for moduleNameMapper — Vite aliases work automatically.
Step 3: Imports
Replace jest.fn()vi.fn(), jest.mock()vi.mock(), jest.spyOn()vi.spyOn(). Or enable globals: true to use without imports.
Step 4: Scripts
Replace "test": "jest""test": "vitest" in package.json. Add "test:ui": "vitest --ui" for a beautiful web interface.
Step 5: Verify
Run npx vitest run. Snapshot format is Jest-compatible so no updates needed. Coverage switches to v8 (faster than istanbul).
// The only import change needed:
- import { jest } from '@jest/globals'
+ import { vi } from 'vitest'

// API is nearly identical:
- const mockFn = jest.fn()
+ const mockFn = vi.fn()

- jest.spyOn(api, 'fetchUser')
+ vi.spyOn(api, 'fetchUser')

- jest.useFakeTimers()
+ vi.useFakeTimers()

8. CI/CD Integration

Vitest supports multiple reporter formats for CI:

# .github/workflows/test.yml
name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci

      # Unit tests
      - run: npx vitest run --reporter=junit --outputFile=results.xml

      # Coverage
      - run: npx vitest run --coverage

      # Browser tests (requires Playwright)
      - run: npx playwright install --with-deps chromium
      - run: npx vitest run --project=browser

Workspace Mode for Monorepos

Vitest 4.0 allows configuring workspaces directly in vitest.config.ts instead of a separate file:

export default defineConfig({
  test: {
    workspace: [
      { test: { name: 'unit', environment: 'node' } },
      { test: { name: 'browser', browser: { enabled: true } } }
    ]
  }
})

9. Best Practices with Vitest

Practice Do Avoid
Naming *.test.ts or *.spec.ts next to source file Separate __tests__ folder far from source
Mocking vi.mock() for external modules, vi.spyOn() for methods Mocking everything — tests lose meaning
Environment happy-dom (2-3x faster than jsdom) jsdom when you don't need full browser compat
Coverage Provider v8 (native, fast) istanbul (slower, requires instrumentation)
Browser tests Only for components needing real DOM All tests in browser — slows CI

10. Conclusion

Vitest 4.0 isn't just "faster Jest" — it's a testing framework designed for the modern ecosystem with native ESM, zero-config TypeScript, and Browser Mode that actually works. With Vue 3, Nuxt 4, and Vite as the standard, Vitest is the most natural and efficient testing choice.

If you're starting a new project, Vitest is the default — no debate needed. If you're currently using Jest, migration takes about 30 minutes for most projects — and you'll immediately feel the difference with near-instant watch mode.

References:
Vitest 4.0 Official Blog · Vitest 3 vs Jest 30: Testing in 2026 — PkgPulse · Vitest in 2026 — DEV Community · Vitest GitHub Repository