JSDOM vs Happy DOM

5 min read
Testing JavaScript Tooling

When you run tests for frontend code in Node.js, you need something that pretends to be a browser. JSDOM and Happy DOM are the two main options. They take different approaches and the choice between them affects test speed, compatibility, and how much you can trust your test results.

What these tools do

Neither JSDOM nor Happy DOM is a real browser. They are JavaScript implementations of the DOM and browser APIs that run inside Node.js. When your test imports a React component and renders it, one of these libraries provides the document, window, and everything else your code expects to find in a browser environment.

Without them, any code that touches document.createElement, window.location, or localStorage would throw. They make it possible to test UI code without launching Chrome.

JSDOM

JSDOM has been around since 2010. It is the default environment in Jest and the most widely used DOM implementation for testing. It aims for spec compliance — it implements the DOM, HTML parsing, CSS selectors, events, and many Web APIs according to the official standards.

This thoroughness comes at a cost. JSDOM is slow to start up and uses a lot of memory. In large test suites with hundreds of files, the overhead adds up. Each test file creates a new JSDOM instance, and initialization is not cheap.

The upside is reliability. If something works in JSDOM, it almost certainly works in a real browser. The API coverage is deep enough that most frontend code runs without issues or workarounds.

Happy DOM

Happy DOM is a newer project that prioritizes speed over full spec compliance. It implements the most commonly used browser APIs but skips some of the edge cases that JSDOM handles.

The performance difference is significant. Happy DOM starts faster and uses less memory. Test suites that take minutes with JSDOM can run in a fraction of the time with Happy DOM. This is why Vitest adopted it as a recommended environment.

The trade-off is that some APIs are missing or behave slightly differently. If your code relies on advanced CSS features, MutationObserver edge cases, or less common Web APIs, you may hit gaps. In practice, most typical React or Vue component tests work fine.

Comparison

JSDOM Happy DOM
Speed Slower startup and execution 2-3x faster in most benchmarks
Spec compliance High — follows W3C/WHATWG specs closely Good enough — covers common APIs
Default in Jest Vitest (recommended)
Memory usage Higher Lower
Maturity 15+ years, battle-tested Newer, actively developed
CSS support Partial (no layout engine) Partial (no layout engine)
ESM support Can have issues with Node.js ESM Better ESM compatibility

When to use which

Use JSDOM if you are using Jest, if your tests depend on precise DOM behavior, or if you are testing a library that needs to work across many browser quirks. JSDOM is also the safer choice for legacy projects where switching environments could surface subtle differences.

Use Happy DOM if you are using Vitest, if test speed is a priority, or if your tests are mostly rendering components and checking output. For the typical case of testing React components with Testing Library, Happy DOM works well and runs noticeably faster.

Neither replaces a real browser. Both JSDOM and Happy DOM skip layout, painting, and many browser behaviors. For visual testing, accessibility audits, or anything involving actual rendering, use a real browser via Playwright or Cypress.

Switching between them

In Vitest, switching is a one-line config change:

// vitest.config.ts
export default defineConfig({
  test: {
    environment: 'happy-dom', // or 'jsdom'
  },
});

In Jest, JSDOM is the default. Using Happy DOM with Jest requires a custom environment package. Most people who want Happy DOM are already on Vitest.

If you are starting a new project with Vitest, start with Happy DOM. If a test fails in a way that seems environment-related, you can always switch that specific file to JSDOM using a docblock comment:

// @vitest-environment jsdom

This gives you the speed of Happy DOM by default with JSDOM as a fallback where needed.