Skip to main content

happy-dom vs jsdom in 2026: Test Environment Performance

·PkgPulse Team

TL;DR

happy-dom is 2-4x faster than jsdom and works great for most React/Vue component tests with Vitest. jsdom (~27M weekly downloads) has been the standard DOM simulation environment for 10+ years — more complete, more battle-tested. happy-dom (~3M downloads) sacrifices edge-case completeness for speed. For Vitest users with large test suites, happy-dom is worth the switch. For Jest or tests that rely on obscure DOM APIs, stick with jsdom.

Key Takeaways

  • jsdom: ~27M weekly downloads — happy-dom: ~3M (npm, March 2026)
  • happy-dom is 2-4x faster — significant for large test suites
  • jsdom has better DOM compatibility — more complete browser API support
  • happy-dom is Vitest's recommended default — Vitest docs suggest happy-dom
  • Both simulate a browser environment — neither runs a real browser (use Playwright for that)

What These Libraries Do

Both jsdom and happy-dom implement the browser DOM and Web APIs in Node.js, allowing component tests to run without a real browser:

Test runner (Node.js)
  ↓
DOM simulation (jsdom OR happy-dom)
  ↓
Your React/Vue component
  ↓
Rendered virtual DOM
  ↓
Assertions via Testing Library

Neither is a browser — they're approximations. For true cross-browser testing, use Playwright or Cypress.


Configuration

// Vitest — switch environment per test file or globally
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // Global setting
    environment: 'happy-dom', // or 'jsdom'

    // Per-file override via comment in test file:
    // @vitest-environment jsdom
    // @vitest-environment happy-dom
  },
});
// Jest — configure in jest.config.js
module.exports = {
  testEnvironment: 'jsdom', // Default for Jest
  // happy-dom for Jest: jest-environment-happydom package
  testEnvironment: 'jest-environment-happydom',
};

Performance Difference

Test suite: 500 React component tests using Testing Library

Environment | Time    | Difference
------------|---------|------------
happy-dom   | 8.2s    | baseline
jsdom       | 24.1s   | 2.9x slower

Test suite: 100 tests, simple components

Environment | Time    | Difference
------------|---------|------------
happy-dom   | 1.4s    | baseline
jsdom       | 3.1s    | 2.2x slower

The speedup compounds with test suite size. Teams with thousands of component tests see the biggest gains.


API Compatibility Examples

// APIs both support well
document.createElement('div');
document.querySelector('.class');
element.addEventListener('click', handler);
element.getBoundingClientRect();
window.localStorage.setItem('key', 'value');
fetch('https://api.example.com'); // via msw mocking

// APIs where jsdom has better support:
// - Complex CSS (computed styles, CSS variables, media queries)
// - SVG manipulation
// - Canvas API (partial in both)
// - Shadow DOM (improving in happy-dom)
// - CSS custom properties (getPropertyValue)

// APIs happy-dom handles well that it used to miss:
// - ResizeObserver (added in recent versions)
// - IntersectionObserver (added in recent versions)
// - MutationObserver
// - CustomEvent
// - FormData

Real-World Compatibility

// This works in both environments:
import { render, screen, fireEvent } from '@testing-library/react';
import { useState } from 'react';

function SearchInput({ onSearch }) {
  const [value, setValue] = useState('');
  return (
    <input
      type="search"
      value={value}
      onChange={e => setValue(e.target.value)}
      onKeyDown={e => e.key === 'Enter' && onSearch(value)}
      placeholder="Search..."
    />
  );
}

test('calls onSearch on Enter', async () => {
  const onSearch = vi.fn();
  render(<SearchInput onSearch={onSearch} />);
  const input = screen.getByPlaceholderText('Search...');
  await userEvent.type(input, 'playwright{enter}');
  expect(onSearch).toHaveBeenCalledWith('playwright');
});

// Works identically in happy-dom and jsdom ✓
// Edge case where jsdom is more reliable:
test('CSS custom properties', () => {
  const div = document.createElement('div');
  div.style.setProperty('--my-color', 'red');
  document.body.appendChild(div);

  // jsdom: works reliably
  // happy-dom: may have inconsistencies with getComputedStyle + custom props
  const computed = getComputedStyle(div);
  expect(computed.getPropertyValue('--my-color')).toBe('red');
});

When happy-dom Falls Short

// CSS animation / transition testing — both are limited
// Neither environment runs actual CSS animations

// Canvas API — partial in both
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// ctx.drawImage, ctx.getImageData may not work correctly

// Complex layout (getBoundingClientRect with real dimensions)
// Neither environment computes real layout — always returns zeros
// For layout testing, use Playwright with real browser

// window.matchMedia — needs manual mock in both
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});

Choosing Between Them

Are you using Vitest?
  YES → Try happy-dom first (it's the recommended default)
  NO  → Use jsdom (better Jest integration, more documentation)

Does your test suite have 500+ component tests?
  YES → happy-dom's speed gain is significant
  NO  → Both are fast enough; pick based on compatibility

Do your tests use complex CSS, SVG, or Canvas APIs?
  YES → Use jsdom (more complete implementation)
  NO  → happy-dom works fine

Are you hitting happy-dom compatibility issues?
  YES → Fall back to jsdom for specific files via @vitest-environment comment
  NO  → Stick with happy-dom

Practical Setup for Vitest

// vitest.config.ts — recommended setup
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'happy-dom', // Fast default
    setupFiles: ['./src/test/setup.ts'],
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';
// This adds toBeInTheDocument, toHaveValue, etc.
// Works with both happy-dom and jsdom
// Override per file for edge cases:
// src/components/chart.test.tsx
// @vitest-environment jsdom
// (Add this comment at top of file for jsdom specifically)

import { render, screen } from '@testing-library/react';
// ...tests that need more complete DOM support

Compare happy-dom and jsdom package health on PkgPulse.

Comments

Stay Updated

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