ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 82

  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.

Aditya Challa·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

  • https://github.com/jsdom/jsdom/releases
  • https://github.com/jsdom/jsdom/releases/tag/v30.1.2
  • https://github.com/capricorn86/happy-dom
  • https://github.com/capricorn86/happy-dom/releases
  • https://vitest.dev/guide/environment
  • https://www.npmjs.com/package/jsdom
  • https://www.npmjs.com/package/happy-dom
  • https://www.npmjs.com/package/vitest

Related

  • https://www.shoppercove.com/blog/playwright-1-63-test-locks-october-2026
  • https://www.shoppercove.com/blog/vite-8-3-2-renderbuilturl-bundled-dev-sourcemaps-october-2026
  • https://www.shoppercove.com/blog/vite-plus-1-0-unified-toolchain-october-2026
  • https://www.shoppercove.com/blog/nodejs-26-lts-october-2026-schedule-change
  • https://www.shoppercove.com/blog/react-19-3-view-transitions-fragment-refs-2026
  • https://www.shoppercove.com/blog/chrome-155-stable-jpeg-xl-css-changes-october-2026
  • https://www.shoppercove.com/blog/pnpm-12-9-1-rust-rewrite-wasm-split-october-2026
  • https://www.shoppercove.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.

Related links

  • Plate 12

    Yarn Berry vs pnpm: 1.3s vs 0.24s Install

    Yarn 4.9.2 (Berry PnP + node-modules linker) vs pnpm 12.9.1 on one 22-dependency React+Vite app. Isolated caches (Yarn enableGlobalCache false; pnpm --store-dir). Warm medians (5): Yarn PnP 1.307 s, Yarn nm 2.471 s, pnpm 0.240 s. Lock+empty medians 1.656 / 2.789 / 0.725 s. Clean disk: Yarn PnP 246 MB; Yarn nm modules 189 MB; pnpm hardlink project 167 MB. Fixture /workspace/bench-yarn-pnpm/. Distinct from pnpm-vs-npm-vs-bun (no Yarn).

    5 Oct 2026

  • Plate 48

    Zod vs Valibot: 1.14M vs 0.97M Parses

    zod 4.6.5 vs valibot 1.5.0 on one nested user schema (uuid/email/age/tags/profile/enum). Warm medians after 2k warmup + 9×50k: zod.parse 1,144,494 ops/s vs valibot.parse 968,187 (~1.18×). safeParse fail: valibot 697,760 vs zod 415,523 (~1.7×). esbuild minify+gzip9: 452,999/92,636 B vs 85,126/15,422 B. Package trees ~6.1 MB vs ~1.9 MB. Fixture /workspace/bench-zod-valibot/.

    5 Oct 2026

  • Plate 55

    mozjpeg vs libvips: 385ms vs 31ms Encode

    mozjpeg cjpeg 3.3.1 (mozjpeg npm 8.0.0) vs libvips jpeg via sharp 0.35.5 (mozjpeg:false) on one 1920×1080 JPEG → q80. Warm medians (7 rounds after 1 warm-up): cjpeg 385 ms / 675,128 B vs libvips jpeg 31 ms / 675,714 B (~12×). Context: sharp mozjpeg:true 308 ms / 546,546 B. Fixture /workspace/bench-mozjpeg-libvips/. cjpeg timed with spawn + PPM read.

    5 Oct 2026

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
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove