happy-dom vs jsdom in 2026: Test Environment Performance
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.
See the live comparison
View happy dom vs. jsdom on PkgPulse →