無駄なJS書きたくない!そんな魔法ライブラリhtmxを紹介してみる
「フロントエンド開発、複雑すぎない?」
ちょっとした非同期通信やUIの部分更新をしたいだけなのに、ReactやVueを導入するのは大げさすぎる気がする。かといって、バニラJSで fetch してDOMをゴリゴリ操作するコードを書きまくるのもしんどい……。そんな「フロントエンド疲れ」を感じたことはありませんか?
そんな悩みに対して、「HTMLをもっと賢くすればいいじゃないか」という発想で生まれたのが htmx です。GitHubのスター数は2026年3月時点で 47,000超 を誇り、世界中の開発者から注目を集めています。
今回は、htmxの思想・使い方・バニラJSやReactとの比較・向き不向きまで、公式サイトの言葉を交えながらたっぷり解説していきます!
htmxってなに?
一言でいうと、「HTMLのタグに hx-* 属性を追加するだけで、AJAX通信・部分更新・CSSトランジション・WebSocketなどが使えるようになるJavaScriptライブラリ」 です。
公式サイトのトップには、こんなキャッチコピーが掲げられています。
htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext
— htmx.org
(意訳:htmxは、属性を使ってHTMLから直接AJAX・CSSトランジション・WebSocket・Server Sent Eventsにアクセスできるようにします。これにより、ハイパーテキストのシンプルさと強力さを活かして、モダンなUIを構築できます。)
さらに、ライブラリとしての特徴も公式が明記しています。
- サイズは 約16KB(min.gz'd)
- 依存関係ゼロ
- 拡張可能
- Reactと比較してコードベースを 67%削減 した実績あり
これだけ聞いても「ホントに?」という感じですよね。実際にコードを見てみましょう。
百聞は一見に如かず:コードを見てみよう
例:ボタンをクリックしてサーバーからデータを取得し、画面の一部を更新する
バニラJSの場合
<button id="load-btn">データを読み込む</button>
<div id="result-area"></div>
<script>
document.getElementById('load-btn').addEventListener('click', async () => {
const response = await fetch('/api/data');
const html = await response.text();
document.getElementById('result-area').innerHTML = html;
});
</script>
要素を取得して、イベントリスナーを登録して、fetchして、DOMを更新して……。ちょっとしたことなのに、書くコードが多いですよね。
htmxの場合
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<button hx-get="/api/data" hx-target="#result-area">
データを読み込む
</button>
<div id="result-area"></div>
えっ、これだけ!?
はい、これだけです。JavaScriptは1行も書いていません。hx-get でリクエスト先を指定し、hx-target で結果を反映する要素を指定するだけ。直感的でめちゃくちゃシンプルですよね。
公式ドキュメントでは、この仕組みをこう説明しています。
htmx is a library that allows you to access modern browser features directly from HTML, rather than using javascript.
htmxの根本思想:なぜ生まれたのか?
htmxの作者Carson Gross氏は、現在のHTMLが持つ「制約」についてこう問いかけています。
- Why should only
<a>&<form>be able to make HTTP requests?- Why should only
click&submitevents trigger them?- Why should only
GET&POSTmethods be available?- Why should you only be able to replace the entire screen?
By removing these constraints, htmx completes HTML as a hypertext
— htmx.org
(意訳:
- なぜ
<a>と<form>だけがHTTPリクエストを送れるのか? - なぜ
clickとsubmitイベントだけがそれをトリガーできるのか? - なぜ
GETとPOSTメソッドしか使えないのか? - なぜ画面全体しか置き換えられないのか?
これらの制約を取り除くことで、htmxはHTMLをハイパーテキストとして完成させる。)
確かに言われてみればその通りですよね。<div> はHTTPリクエストを送れないし、mouseover イベントでフォームを送信することもできない。htmxはこれらの制約を取り払い、「どんな要素でも」「どんなイベントでも」「どんなHTTPメソッドでも」「画面の一部だけを」 更新できるようにしてくれます。
htmxの主要な属性(魔法の呪文たち)
htmxには、HTMLを拡張するための様々な hx-* 属性が用意されています。コアとなる主要な属性を見ていきましょう。
コア属性
| 属性 | 説明 |
|---|---|
hx-get |
指定したURLにGETリクエストを送信する |
hx-post |
指定したURLにPOSTリクエストを送信する |
hx-put |
指定したURLにPUTリクエストを送信する |
hx-patch |
指定したURLにPATCHリクエストを送信する |
hx-delete |
指定したURLにDELETEリクエストを送信する |
hx-trigger |
リクエストをトリガーするイベントを指定する |
hx-target |
レスポンスを挿入するターゲット要素を指定する |
hx-swap |
レスポンスをどのように挿入するかを指定する |
hx-trigger の柔軟さが面白い
hx-trigger は特に強力で、様々なイベントとモディファイアを組み合わせられます。
<!-- 入力から500ms後にリクエスト(デバウンス) -->
<input hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results">
<!-- マウスが乗ったときにリクエスト(一度だけ) -->
<div hx-post="/mouse_entered" hx-trigger="mouseenter once">
[Here Mouse, Mouse!]
</div>
<!-- 2秒ごとにポーリング -->
<div hx-get="/news" hx-trigger="every 2s"></div>
これ、バニラJSで書こうとするとそれなりのコード量になりますよね。htmxなら属性一つで済んでしまいます。
hx-swap でDOM更新の方法を細かく制御
hx-swap では、サーバーから返ってきたHTMLをどのようにDOMに挿入するかを指定できます。
| 値 | 動作 |
|---|---|
innerHTML(デフォルト) |
ターゲット要素の内側を置き換える |
outerHTML |
ターゲット要素ごと置き換える |
beforebegin |
ターゲット要素の直前に挿入する |
afterbegin |
ターゲット要素の最初の子要素として挿入する |
beforeend |
ターゲット要素の最後の子要素として挿入する |
afterend |
ターゲット要素の直後に挿入する |
delete |
レスポンスに関わらずターゲット要素を削除する |
none |
DOMを変更しない |
実践的な例:アクティブサーチ
公式サイトのサンプルにもある「アクティブサーチ」(入力しながらリアルタイムで検索結果が出てくるUI)を見てみましょう。
<h3>
Search Contacts
<span class="htmx-indicator">
<img src="/img/bars.svg" alt=""/> Searching...
</span>
</h3>
<input class="form-control" type="search"
name="search" placeholder="Begin Typing To Search Users..."
hx-post="/search"
hx-trigger="input changed delay:500ms, keyup[key=='Enter'], load"
hx-target="#search-results"
hx-indicator=".htmx-indicator">
<table class="table">
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
</tr>
</thead>
<tbody id="search-results"></tbody>
</table>
このコードで実現していること:
-
hx-post="/search"→ 入力内容を/searchにPOSTする -
hx-trigger="input changed delay:500ms"→ 入力が変化してから500ms後にリクエスト(デバウンス) -
keyup[key=='Enter']→ Enterキーでも即時リクエスト -
load→ ページ読み込み時にも一度リクエスト(初期表示) -
hx-target="#search-results"→ 結果をtbodyに挿入 -
hx-indicator=".htmx-indicator"→ リクエスト中はスピナーを表示
これだけのUIをJavaScript ゼロ行 で実現できるのは、なかなか衝撃的ではないでしょうか。
バニラJS・React・Vueとの比較
さて、ここが本題です。htmxは既存のアプローチと比べて何が嬉しいのか、整理してみましょう。
htmx vs. バニラJS
バニラJSと比べたときのhtmxの最大の利点は、「Locality of Behaviour(振る舞いの局所性)」 です。
htmxの作者Carson Gross氏は、この原則をこう定義しています。
The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code
(コードの単位の振る舞いは、そのコードの単位だけを見て可能な限り明白であるべきだ)
公式サイトでは、htmxとjQueryの比較でこの違いを示しています。
htmxの場合(LoB良好)
<button hx-get="/clicked">Click Me</button>
jQueryの場合(LoB不良)
// JSファイルのどこか
$("#d1").on("click", function(){
$.ajax({ /* AJAX options... */ });
});
<!-- HTMLファイル -->
<button id="d1">Click Me</button>
htmxでは「このボタンを押したら /clicked にGETリクエストが飛ぶ」という振る舞いが、ボタンのタグを見るだけで一目でわかります。一方、バニラJSやjQueryでは、HTMLとJSが分離しているため、「このボタンが何をするのか」を理解するには別ファイルを探し回る必要があります。これが「遠隔作用(Spooky action at a distance)」と呼ばれる問題で、コードベースが大きくなるほどメンテナンスを困難にします。
htmx vs. React / Vue
ReactやVueなどのモダンなJSフレームワークとの比較は少し複雑です。それぞれに向き不向きがあります。
| 観点 | htmx | React / Vue |
|---|---|---|
| 学習コスト | 低い(HTML属性を覚えるだけ) | 高い(JSX/テンプレート構文、状態管理、ライフサイクルなど) |
| ファイルサイズ | 約16KB | 数十〜数百KB(バンドル後) |
| ビルドステップ | 不要(CDNから読み込むだけ) | 必要(webpack, Viteなど) |
| サーバーとのやり取り | HTMLを返す(SSR的) | JSONを返す(API的) |
| 状態管理 | サーバー側で管理(シンプル) | クライアント側で管理(複雑になりがち) |
| 複雑なUI(リアルタイム、相互依存) | 苦手 | 得意 |
| CRUD中心のアプリ | 得意 | やや過剰になりがち |
| チームの採用しやすさ | まだ少ない | 求人・コミュニティが豊富 |
転送サイズ・バンドルサイズの実測比較
「htmxは軽い」とよく言われますが、実際のところどうなのでしょうか?
今回、「アクティブサーチ(入力しながらリアルタイム検索)」 という全く同じ機能を持つデモアプリを、React・Vue・バニラJS・htmxの4パターンで作成し、ビルド後の転送サイズを実測してみました。
- React: Viteでビルド(react + react-domを含む)
- Vue: Viteでビルド(vueを含む)
- Vanilla JS: 外部ライブラリなし、単一のHTMLファイルにJSを直書き
-
htmx: CDNから
htmx.min.jsを読み込み、サーバー(Flask)からHTMLを返す
その結果がこちらです。
▼ 合計転送サイズ比較(gzip圧縮後)
| ライブラリ | JS (gzip) | CSS (gzip) | HTML (gzip) | 合計転送サイズ (gzip) |
|---|---|---|---|---|
| React (Vite) | 58.3 KB | 0.8 KB | 0.3 KB | 59.3 KB |
| Vue (Vite) | 23.8 KB | 1.4 KB | 0.3 KB | 25.5 KB |
| htmx (CDN) | 16.2 KB | 0.0 KB | 0.4 KB | 16.6 KB |
| Vanilla JS | 0.0 KB | 0.0 KB | 0.9 KB | 0.9 KB |
当然ながら、何もライブラリを使わないバニラJSが最軽量(0.9KB)ですが、htmxはVueよりも軽く、Reactの約1/3のサイズ(16.6KB) に収まっています。
さらに、ライブラリ本体のみのサイズ(minified / gzip)を比較すると、htmxの軽量さが際立ちます。
▼ ライブラリ本体サイズ比較(minified / gzip)
React(react + react-dom)がgzip後で約46.5KBあるのに対し、htmxはわずか 16.2KB。Vue(約16.0KB)とほぼ同等の軽さです。
しかも、htmxはビルドツール(Viteやwebpackなど)を一切必要とせず、<script> タグを1行書くだけでこの恩恵を受けられます。
サーバーがHTMLを返すという思想
特に注目したいのは 「サーバーとのやり取り」 の違いです。
ReactなどのSPAでは、サーバーはJSONを返し、クライアント側のJSがそれを解釈してHTMLを構築します。一方、htmxでは サーバーが直接HTMLの断片を返します。
Note that when you are using htmx, on the server side you typically respond with HTML, not JSON. This keeps you firmly within the original web programming model, using Hypertext As The Engine Of Application State without even needing to really understand that concept.
(htmxを使う場合、サーバー側では通常JSONではなくHTMLを返します。これにより、Hypertext As The Engine Of Application State(HATEOAS)という概念を深く理解しなくても、Webの本来のプログラミングモデルにしっかり留まることができます。)
これはつまり、クライアント側に複雑な状態管理ライブラリ(Redux、Zustandなど)が不要になる、ということです。「どのデータをどう表示するか」はサーバー側のテンプレートエンジンが担当し、htmxはその結果をDOMに挿入するだけ。この役割分担のシンプルさが、コードベースの削減につながります。
実際にReactからhtmxへ移行した事例
「理論はわかったけど、実際のところどうなの?」という疑問に答えてくれる事例があります。
フランスのSaaS企業「Contexte」は、2年かけて構築したReact製のUIを、Djangoテンプレートとhtmxへ移行しました。その結果がこちら。
- The effort took about 2 months (with a 21K LOC code base, mostly JavaScript)
- No reduction in the application's user experience (UX)
- They reduced the code base size by 67% (21,500 LOC to 7200 LOC)
- They reduced their total JS dependencies by 96% (255 to 9)
- They reduced their web build time by 88% (40 seconds to 5)
- First load time-to-interactive was reduced by 50-60% (from 2 to 6 seconds to 1 to 2 seconds)
- Web application memory usage was reduced by 46% (75MB to 45MB)
(意訳:
- 移行にかかった期間:約2ヶ月(21,000行のJSコードベース)
- ユーザー体験(UX)の低下なし
- コードベースのサイズが67%削減(21,500行 → 7,200行)
- JS依存関係が96%削減(255個 → 9個)
- ビルド時間が88%削減(40秒 → 5秒)
- 初回読み込み時間(TTI)が50〜60%短縮(2〜6秒 → 1〜2秒)
- メモリ使用量が46%削減(75MB → 45MB)
)
さらに面白いのが、チーム構成への影響です。
Reactを使っていた頃は、バックエンドエンジニアとフロントエンドエンジニアが明確に分かれていました。ところが、htmxへ移行した後は チーム全員がフルスタックエンジニアになった とのこと。サーバーサイドのテンプレートとhtmxの属性を書くだけでフロントエンドが完結するため、バックエンドエンジニアでも容易にフロントエンドの実装ができるようになったわけです。
もちろん、Contexteのアプリは「テキストと画像を表示するメディアアプリ」という、htmxが最も得意とするタイプでした。すべてのアプリでこれほどの効果が出るわけではありませんが、それでもこの数字は説得力がありますよね。
htmxが向いているケース・向いていないケース
公式サイトのエッセイ「When Should You Use Hypermedia?」を参考に整理しました。
向いているケース
テキスト・画像がメインのUI
ブログ、ニュースサイト、ドキュメントサイトなど。コンテンツを表示することが主目的のアプリケーションは、htmxと相性抜群です。
CRUD操作が中心のアプリ
フォームを入力してデータベースに保存する、という伝統的なWebアプリケーションのパターン。RailsやDjangoなどのサーバーサイドフレームワークと組み合わせると特に強力です。
更新が特定のブロック内に収まるUI
画面全体ではなく、特定のセクションだけを独立して更新したい場合。例えば「コメント一覧」「検索結果」「通知バッジ」などのコンポーネント単位の更新。
ディープリンクや初回レンダリング速度が重要なアプリ
htmxはHTMLをサーバーから直接返すため、SEOや初回表示速度の面でSPAより有利です。
向いていないケース
動的で複雑な相互依存関係があるUI
Googleスプレッドシートのように、1つのセルの変更が他の多くのセルに即座に影響を与えるようなUI。htmxはサーバーへのラウンドトリップが必要なため、このような細粒度のリアルタイム更新には不向きです。
オフラインでの動作が必要なアプリ
htmxはサーバーとの通信を前提としているため、オフラインファーストなPWAには適しません。
UIの状態が極めて頻繁に更新されるアプリ
Googleマップのようにマウスの動きに合わせて常に画面が書き換わるアプリや、リアルタイムゲームなど。
コピー&ペーストで使えるUIコンポーネントが必要な場合
ShadCNのようなReact向けのコンポーネントライブラリは、htmxとは直接統合できません。
htmxのインストール方法
htmxの導入は驚くほど簡単です。
CDN経由(最も手軽)
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"></script>
これをHTMLの <head> や <body> の末尾に追加するだけ。ビルドステップ不要、設定ファイル不要 です。
npm経由
npm install htmx.org@2.0.8
import 'htmx.org';
対応サーバーサイド言語
htmxはサーバーからHTMLを返すだけでよいため、どんなサーバーサイド言語とも組み合わせられます。
- Python(Django、Flask、FastAPI)
- Ruby(Ruby on Rails)
- Go(Echo、Gin)
- Java(Spring Boot)
- PHP(Laravel)
- Node.js(Express)
- その他なんでも
「フロントエンドとバックエンドを別々に開発してAPIで繋ぐ」という構成を取らなくていいので、小〜中規模のチームには特に嬉しいですね。
その他の便利な機能
hx-boost:既存のリンクやフォームをAJAX化
<div hx-boost="true">
<a href="/blog">Blog</a>
<form action="/search" method="POST">...</form>
</div>
hx-boost="true" を親要素に付けるだけで、その配下のすべてのリンクとフォームが自動的にAJAXリクエストに変換されます。ページ遷移が画面全体のリロードではなく、<body> の部分更新になるため、SPAのようなスムーズな体験が得られます。
さらに、JavaScriptが無効な環境でも通常のリンク・フォームとして動作する(プログレッシブエンハンスメント)という特徴もあります。
ブラウザ履歴の管理
<a hx-get="/blog" hx-push-url="true">Blog</a>
hx-push-url="true" を付けると、AJAXリクエストを行いながらもブラウザのURLバーとヒストリーが更新されます。ブラウザの「戻る」ボタンも正しく機能します。
確認ダイアログ
<button hx-delete="/account"
hx-confirm="本当にアカウントを削除しますか?">
アカウントを削除
</button>
hx-confirm を付けるだけで、リクエスト送信前に確認ダイアログが表示されます。
ローディングインジケーター
<button hx-get="/slow-endpoint">
データを取得
<img class="htmx-indicator" src="/spinner.gif" alt="Loading...">
</button>
htmx-indicator クラスを付けた要素は、リクエスト中のみ表示されます(デフォルトは opacity: 0、リクエスト中は opacity: 1)。
「htmxはただのJavaScriptフレームワークじゃないの?」という疑問
htmxを初めて聞いた人からよく出る疑問が「結局、別のJSフレームワークを覚えないといけないんじゃ?」というものです。
公式サイトのエッセイ「Is htmx Just Another JavaScript Framework?」では、この点についてこう述べています。
htmx is for writing HTML. [...] React, Svelte, Solid, and so on have you write JS(X) that the framework converts into HTML; htmx just has you write HTML.
(htmxはHTMLを書くためのものです。ReactやSvelte、SolidなどはフレームワークがHTMLに変換するJSXを書かせますが、htmxはただHTMLを書かせるだけです。)
つまり、htmxを使うにあたって覚えることは:
-
hx-get,hx-postなどのHTTP動詞属性 -
hx-triggerでのイベント指定 -
hx-targetとhx-swapでのDOM更新制御
これだけです。JSXの書き方、コンポーネントのライフサイクル、状態管理の仕組み……そういったものを一切覚える必要がありません。HTMLを書ける人なら、数時間で使い始められます。
また、htmxはビルドステップが不要なため、webpackやViteの設定に悩む必要もありません。依存関係の管理(node_modules地獄)からも解放されます。
まとめ:「ちょうどいい」を選ぶ勇気
htmxは、「何でもかんでもReactでSPAにする」という現代のフロントエンド開発の風潮に対するアンチテーゼです。
公式サイトのエッセイには、こんな詩的な一節があります。
javascript fatigue:
longing for a hypertext
already in hand
(JavaScript疲れ:すでに手の中にあるハイパーテキストへの憧れ)
世の中の多くのWebアプリケーション、特に社内ツールや管理画面、CRUDアプリ、コンテンツサイトなどは、実はReactほどの複雑さを必要としていないかもしれません。そういったアプリケーションに対して、htmxは 「ちょうどいい塩梅」 の選択肢を提供してくれます。
もちろん、複雑な状態管理やリアルタイムなインタラクションが必要なアプリには、引き続きReactやVueが適しています。大切なのは、「このアプリに本当に必要な複雑さはどのくらいか?」 を問い直すことです。
「JSを書く量を減らしたい」「もっとシンプルにWebアプリを作りたい」「サーバーサイドのエンジニアがフロントエンドもサクッと書けるようにしたい」と思っている方は、ぜひ一度htmxを触ってみてください。HTMLの持つ本来のパワーに、きっと驚くはずです!
Discussion