「happy-dom」vs「jsdom」 機能比較とパフォーマンス測定

に公開

はじめに

現在、React Router を使ってウェブアプリを作っているのですが、テストライブラリには happy-dom を使っています。
当初は jsdom を使っていたのですが、画面遷移時のテストで上手くいかない問題に遭遇しました。
具体的には、リダイレクト先の URL をテストで確認したかったのですが、jsdom で実現するには結構難航しそうでした。
happy-dom に切り替えることで無事解消されましたが、そもそも happy-dom と jsdom にはどんな違いがあるのか気になり、今回調べてみました。

また、以下の記事で「happy-dom の方がパフォーマンスが良い」という話を知り、本当に優れているのか実際にベンチマークを取って比較してみました。

https://zenn.dev/odan/scraps/eae43b327f0e33

TL;DR(結論)

  • happy-dom: 速度重視(jsdom の 2〜5 倍高速)、基本的なテストに最適
  • jsdom: 完全性重視、複雑なブラウザ機能のテストに最適
  • React Router テスト: happy-dom の方がナビゲーション実装が充実しており、テストしやすい

基本情報の比較

項目 happy-dom jsdom
週間ダウンロード数 2,420,127 32,906,228
GitHub Stars 4,023 21,260
ライセンス MIT MIT
Node.js 要件 - v20+
主な特徴 高速、軽量 完全性、標準準拠

jsdom の方が圧倒的に普及していますが、happy-dom も急速に成長しています。

機能比較

共通でサポートされている機能

両方とも以下の基本機能はサポートしています:

  • ✅ DOM 操作(createElement, appendChild, querySelector 等)
  • ✅ イベントシステム
  • ✅ Mutation Observer
  • ✅ Tree Walker
  • ✅ Fetch API
  • ✅ Custom Elements(Web Components)
  • ✅ Shadow DOM
  • ✅ WHATWG DOM/HTML 標準

happy-dom 特有の機能

  • Declarative Shadow DOM(サーバーサイドレンダリング対応)
  • ✅ 超高速 DOM 操作
  • ✅ より軽量なメモリフットプリント
  • ✅ Bun、Vitest、Jest との統合

jsdom 特有の機能

  • ✅ より完全なブラウザ API 実装
  • ✅ スクリプト実行制御(dangerously、outside-only モード)
  • ✅ Cookie Jar 管理
  • ✅ カスタムリソースローディング
  • ✅ Virtual Console 機能
  • ✅ フレーム、iframe、スタイルシート、画像の読み込み

制限事項

両方に共通する制限

  • 視覚的なレンダリングができない
    • 実際に画面に描画することはできません
    • element.offsetWidthなどが正確な値を返しません
  • ❌ レイアウト計算なし
  • ❌ CSS の完全なサポートなし

happy-dom の制限

  • ⚠️ 一部のブラウザ API が未実装
    • HTMLOptionElementがグローバルスコープに存在しない
  • ⚠️ イベントの発火順序が正しくない場合がある
  • ⚠️ IntersectionObserverResizeObserver未対応

jsdom の制限

  • ⚠️ パフォーマンスが happy-dom より遅い
  • ⚠️ メモリ使用量が多い
  • ⚠️ Declarative Shadow DOM 未対応

ブラウザ API 対応状況

主要 API 対応表

API happy-dom jsdom
querySelector/querySelectorAll
createElement/appendChild
addEventListener
innerHTML/outerHTML
MutationObserver
IntersectionObserver ⚠️ 限定的
ResizeObserver
Custom Elements
Declarative Shadow DOM
Canvas API ⚠️ 要パッケージ

フォーム関連

API happy-dom jsdom
HTMLFormElement
HTMLInputElement
HTMLSelectElement
HTMLOptionElement
FormData

React Router テストの違い

なぜ happy-dom の方がテストしやすいのか?

React Router のテストでhappy-dom だと動くのに、jsdom だとエラーが出るという経験はありませんか?

jsdom の問題

jsdom では以下のようなエラーが発生します:

Error: Not implemented: navigation (except hash changes)

原因:

  • jsdom はwindow.location.assign()window.location.replace()などのナビゲーションメソッドを実装していない
  • React Router がページ遷移時にこれらを使用するとエラーになる
  • History API の実装も不完全

happy-dom の利点

  • ナビゲーション機能がより多く実装されている
  • React Router v6 の History API 依存にも対応
  • 追加のモックなしでテストが動作

jsdom での回避策

どうしても jsdom を使いたい場合は、以下のようにモックする必要があります:

// テストセットアップファイル(setupTests.js等)
delete window.location;
window.location = {
  assign: jest.fn(),
  replace: jest.fn(),
  href: "",
};

// または jest-location-mock パッケージを使用
import "jest-location-mock";

React Router テストの推奨環境

{
  "devDependencies": {
    "happy-dom": "^19.0.0",
    "vitest": "^1.0.0",
    "@testing-library/react": "^14.0.0"
  }
}
// vitest.config.js
export default {
  test: {
    environment: "happy-dom",
  },
};

パフォーマンス比較

実際にベンチマークを取ってみました。

ベンチマーク環境

// 測定コード例
function measure(name, fn) {
  const start = process.hrtime.bigint();
  fn();
  const end = process.hrtime.bigint();
  return Number(end - start) / 1_000_000; // ms
}

結果

テスト項目 happy-dom jsdom 速度差
DOM 初期化 5.67ms 29.98ms 5.29 倍高速
要素の作成と追加(1000 個) 7.98ms 16.66ms 2.09 倍高速
querySelector(10000 回) 114.38ms 117.56ms 1.03 倍高速
属性の読み書き(10000 回) 18.27ms 25.37ms 1.39 倍高速
innerHTML(1000 回) 18.94ms 67.40ms 3.56 倍高速

使い分けガイド

happy-dom を選ぶべき場合

  • ✅ テストの実行速度を最優先したい
  • ✅ CI パイプラインで大量のテストを高速に実行したい
  • ✅ 基本的な DOM 操作のテストのみで十分
  • ✅ メモリ使用量を抑えたい
  • ✅ Vitest、Bun などの最新ツールと統合したい
  • React Router のテストを簡単にしたい 🎯
  • ✅ サーバーサイドレンダリング(SSR)で Web Components を使用

jsdom を選ぶべき場合

  • ✅ ブラウザ環境の完全な再現が必要
  • ✅ より正確な Web 標準への準拠が必要
  • ✅ 複雑な DOM 操作や高度なブラウザ API を使用
  • ✅ Cookie やカスタムリソースローディングが必要
  • ✅ より成熟したライブラリが必要(広く使われている)
  • HTMLOptionElementなど、happy-dom で未サポートの API が必要

実際の移行例

Jest からの移行(happy-dom 使用)

// package.json
{
  "scripts": {
-   "test": "jest"
+   "test": "vitest"
  },
  "devDependencies": {
-   "jest": "^29.0.0",
-   "jsdom": "^22.0.0",
+   "vitest": "^1.0.0",
+   "happy-dom": "^19.0.0"
  }
}
// vitest.config.js
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "happy-dom",
    globals: true,
  },
});

パフォーマンス改善の実例

以下は実際のプロジェクトでの移行結果です:

jsdom使用時:
  ✓ 500 tests passed (2m 15s)

happy-dom使用時:
  ✓ 500 tests passed (45s)

→ 約3倍の高速化 🚀

ベンチマークコード(再現用)

この記事のベンチマーク結果を再現したい方は、以下のリポジトリをご利用ください。

https://github.com/tamoco-mocomoco/jsdom-vs-happy-dom

クローン後、以下のコマンドで実行できます:

npm install
node benchmark.js

まとめ

  • パフォーマンス: happy-dom が圧勝(2〜5 倍高速)
  • 完全性: jsdom の方が標準準拠度が高い
  • React Router テスト: happy-dom の方が簡単(ナビゲーション実装が充実)
  • メモリ効率: happy-dom の方が軽量
  • 成熟度: jsdom の方が広く使われている

個人的な推奨

新規プロジェクトや、特に理由がなければhappy-domを推奨します。理由は:

  1. 圧倒的な速度(テストが快適)
  2. React Router などモダンライブラリとの相性が良い
  3. Vitest 等の新しいツールとの統合がスムーズ
  4. 基本的なテストには十分な機能

ただし、以下の場合はjsdomを選択してください:

  1. 既存の Jest プロジェクトで移行コストが高い
  2. happy-dom で未実装の API が必要
  3. より厳密なブラウザ互換性が必要

参考リンク

Discussion