ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
Topics
sre111localhost lab102performance70python58javascript47bundle size36benchmarking35node.js35web development21typescript19benchmark16content creation15All topics
About
Loading article…

ShopperCove

Hands-on notes on developer tools, written after the tool has actually been run.

All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove

On this page

  1. What I tested
  2. Is happy-dom faster than jsdom in Vitest?
  3. What breaks when you switch to happy-dom?
  4. Does jsdom 30 need a newer Node?
  5. How do I switch without a big-bang change?
  6. Should you switch from jsdom to happy-dom?
  7. Sources
  8. Related
  1. Blog

jsdom vs happy-dom: Should You Switch? 1.9x Faster, 2 Gaps

jsdom vs happy-dom decision with ShopperCove timing in Vitest 5.0.3: 20-file / 60-test suite median 1.65 s on happy-dom 20.14.5 vs 3.11 s on jsdom 30.1.2; heavy DOM 1.33 s vs 1.81 s (jsdom 29.1.1 2.55 s). Compat 8/8 jsdom 30, 6/8 happy-dom, 5/8 jsdom 29.

5 October 2026·6 min read

Summary
On this page
  1. What I tested
  2. Is happy-dom faster than jsdom in Vitest?
  3. What breaks when you switch to happy-dom?
  4. Does jsdom 30 need a newer Node?
  5. How do I switch without a big-bang change?
  6. Should you switch from jsdom to happy-dom?
  7. Sources
  8. Related

On my 20-file, 60-test Vitest 5.0.3 suite, happy-dom 20.14.5 finished in a median 1.65 s versus 3.11 s for jsdom 30.1.2, about 1.9x faster. But happy-dom failed 2 of my 8 DOM behavior checks, so switch only if your tests stay clear of the gaps listed below.

Short answer: use happy-dom for large suites of simple render-click-assert tests, and keep jsdom 30 for anything touching focus, table APIs or computed CSS. No affiliate links in this post.

What I tested

  • Machine: 8 vCPU / 15 GB RAM Linux cloud box, Node 24.21.0, tested 5 Oct 2026 (IST).
  • Runner: Vitest 5.0.3 with Vite 8.3.2, default forks pool, environment set in vitest.config.ts.
  • Environments: jsdom 30.1.2 (released 4 Oct 2026) and happy-dom 20.14.5 in one project; jsdom 29.1.1 in a second, otherwise identical project.
  • Light suite: 20 files × 3 tests (form submit, a 50-item list, a click that flips aria-expanded).
  • Heavy suite: 4 files × 2 tests (build a 2,000-row table with createElement and sort it; parse 2,000 rows through innerHTML and read getComputedStyle on 200 cells).
  • Timing: three runs each, median of Vitest's reported Duration. Wall clock with process start is listed further down.
  • One surprise: my first heavy test sorted rows with tbody.rows. Under happy-dom it crashed with TypeError: undefined is not iterable. I rewrote the timing test to use querySelectorAll('tr') so both environments run the same work, and kept tbody.rows as a separate compatibility check.
  • Not tested: React Testing Library or user-event, memory use, Vitest browser mode, Windows or macOS, a large production suite.

Is happy-dom faster than jsdom in Vitest?

Suitejsdom 30.1.2happy-dom 20.14.5jsdom 29.1.1
Light: 20 files / 60 tests3.11 s (3.07 / 3.11 / 3.14)1.65 s (1.54 / 1.65 / 1.70)2.93 s (3.11 / 2.93 / 2.64)
Heavy: 4 files / 8 tests1.81 s (1.81 / 1.94 / 1.79)1.33 s (1.34 / 1.33 / 1.27)2.55 s (2.50 / 2.55 / 2.56)
Light, wall clock median3.45 s2.00 s3.25 s
Heavy, wall clock median2.14 s1.68 s2.92 s

On the light suite, Vitest said 90–92% of jsdom's time was environment setup, against 82–83% for happy-dom. The win there is per-file startup, so it grows with the number of test files, not the number of assertions. On the heavy suite the gap narrowed to about 1.36x.

jsdom 30 itself is a real upgrade for DOM-heavy tests: the heavy suite went from 2.55 s on 29.1.1 to 1.81 s on 30.1.2, about 29% faster. That matches the 30.1.0 release notes on faster DOM construction and the 30.1.2 fix for slow large-tree builds. The light suite showed no gain (3.11 s vs 2.93 s, and 29.1.1's runs spread from 2.64 s to 3.11 s).

What breaks when you switch to happy-dom?

Checkjsdom 30.1.2happy-dom 20.14.5jsdom 29.1.1
C1 getComputedStyle reads a <style> colorpasspasspass
C2 CSS.escape('a.b')passpassfail (CSS undefined)
C3 .card:has(img) selectorpasspasspass
C4 empty required input checkValidity() is falsepasspasspass
C5 focus() skips a display:none inputpassfailfail
C6 width: 2em at 10px font computes to 20pxpasspassfail (2em)
C7 getBoundingClientRect().width is 0 (no layout)000
C8 tbody.rows and tr.cellspassfailpass
Total8 / 86 / 85 / 8

The happy-dom C8 failure is narrow: table.rows and tr.cells worked in a direct check, but tbody.rows (on HTMLTableSectionElement) returned undefined. The C5 failure matters more for accessibility tests: happy-dom moved focus onto a hidden input, so a test asserting "focus lands on the first visible field" can pass when it should fail.

C7 is a reminder for both: neither environment does layout. If a test depends on real sizes, scroll positions or geometry-based IntersectionObserver, use Vitest browser mode or Playwright. I covered Playwright's newest test-runner change in the Playwright 1.63 post.

Does jsdom 30 need a newer Node?

Yes. jsdom 30 requires Node ^22.22.2 || ^24.15.0 || >=26.0.0. On Node 22.20.0, npm i jsdom@30.1.2 printed EBADENGINE warnings (first for its @asamuzakjp/css-color 7.1.3 dependency). My compat file still passed 8/8 there, but that is unsupported. happy-dom 20.14.5 only asks for Node 20+. If you cannot move Node yet, jsdom 29.1.1 or happy-dom are your options; the Node 26 LTS schedule post covers the upgrade timing.

How do I switch without a big-bang change?

  1. Keep jsdom as the default environment in vitest.config.ts.
  2. Add // @vitest-environment happy-dom at the top of the files that only render, click and read text. Those gain the most, because setup dominates them.
  3. Run the whole suite under both environments in CI for a week (environment from an env var works, as in my config) and compare failures.
  4. Grep your tests for .focus(, tBodies, .rows and getComputedStyle before you move a file. Those were the risky areas here.
  5. If you are already on Vite 8.3.2, no other config change was needed for either environment.

Should you switch from jsdom to happy-dom?

SituationMy pick
Hundreds of small component tests (render, click, assert text)happy-dom, after checking the gaps above
Tests for focus management, table APIs or computed CSSjsdom 30
Stuck on Node below 22.22.2jsdom 29.1.1 or happy-dom
Tests that need real layout or geometryNeither: browser mode or Playwright

Bottom line: happy-dom cut my light suite from 3.11 s to 1.65 s, and that saving scales with file count. It also got 2 of 8 behavior checks wrong, and one of them (focus on hidden inputs) can hide real accessibility bugs. Who should not switch: teams whose tests exist mainly to catch focus and keyboard regressions.

How this was made: I ran every command above on the ShopperCove test box and kept the raw output; the write-up was drafted with AI help and checked against that output.

Sources

  • 01github.com/jsdom/jsdom/releases/tag/v30.1.2
  • 02github.com/capricorn86/happy-dom
  • 03github.com/capricorn86/happy-dom/releases
  • 04vitest.dev/guide/environment
  • 05npmjs.com/package/jsdom
  • 06npmjs.com/package/happy-dom
  • 07npmjs.com/package/vitest

Related

  • 01shoppercove.com/blog/vite-plus-1-0-unified-toolchain-october-2026
  • 02shoppercove.com/blog/react-19-3-view-transitions-fragment-refs-2026
  • 03shoppercove.com/blog/chrome-155-stable-jpeg-xl-css-changes-october-2026
  • 04shoppercove.com/blog/pnpm-12-9-1-rust-rewrite-wasm-split-october-2026
  • 05shoppercove.com/blog/oxlint-1-87-react-suggestions-a11y-fixes-october-2026
jsdom vs happy-domhappy-dom vs jsdom vitestjsdom 30 performancehappy-dom 20 compatibilityvitest environment happy-domjsdom 30 node version

Lab evidence

What I found running this

Sources read 5 Oct 2026 evening (Asia/Calcutta): github.com/jsdom/jsdom/releases (v30.0.0 27 Jul 2026 Node ^22.22.2||^24.15.0||>=26; v30.1.0 17 Sep perf; v30.1.2 4 Oct 2026 ~14:45 IST large-tree slowdown fix, focus display:none fix); npm jsdom latest 30.1.2, happy-dom 20.14.5 (engines node >=20), vitest 5.0.3 (peer jsdom/happy-dom '*'). Hands-on on box (8 vCPU/15 GB, Node 24.21.0, Vitest 5.0.3 + Vite 8.3.2, forks pool, 3 runs, Vitest Duration): light 20 files/60 tests: jsdom 30.1.2 3.07/3.11/3.14 s; happy-dom 1.54/1.65/1.70 s; jsdom 29.1.1 3.11/2.93/2.64 s. Heavy 4 files/8 tests (2,000-row tables, 200 getComputedStyle): jsdom 30 1.81/1.94/1.79 s; happy-dom 1.34/1.33/1.27 s; jsdom 29 2.50/2.55/2.56 s. Wall medians light 3.45/2.00/3.25 s, heavy 2.14/1.68/2.92 s. Compat 8 checks: jsdom 30 8/8; happy-dom 6/8 (fails focus() on display:none input, tbody.rows undefined while table.rows/tr.cells work); jsdom 29.1.1 5/8 (no CSS.escape, focus hidden, 2em not converted to px). Node 22.20.0: npm EBADENGINE for jsdom 30 deps; compat still 8/8 (unsupported). No ShopperCove production suite changed. No affiliate.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Keep reading

  • Python partition vs split: Which One Should You Use?

    Both cut a string at a separator, but they fail in different ways. We ran them on Python 3.13.5 to show where partition() is safer and where split() is the right tool.

    Summary2 Nov 20263 min read

  • toSorted vs sort JavaScript: Which One Copies Your Array?

    What changes when you swap sort() for toSorted(), with a real 1-million-item Node.js benchmark and the support table.

    Summary1 Nov 20263 min read

  • How to Reorder a YouTube Playlist, and Why Won't It Drag?

    Dragging videos is the official way to change a playlist's order. Here are the steps from YouTube Help, where to find them in YouTube Studio, and the cases where reordering will not work.

    Summary31 Oct 20263 min read