htmx — Xây dựng ứng dụng web động mà không cần JavaScript framework
Posted on: 4/25/2026 5:15:16 PM
Table of contents
1. htmx là gì?
htmx là một thư viện JavaScript siêu nhẹ (~14KB gzip) cho phép bạn xây dựng ứng dụng web động trực tiếp từ HTML — không cần viết JavaScript, không cần build step, không cần virtual DOM. Thay vì tải cả một framework nặng nề rồi render UI trên client, htmx để server trả về HTML fragments và swap chúng vào đúng vị trí trong DOM.
Triết lý cốt lõi của htmx rất đơn giản: HTML vốn đã là một hypermedia, nhưng bị giới hạn bởi chỉ có <a> và <form> mới gửi được HTTP request, chỉ có click và submit mới trigger được, và chỉ có GET/POST mới dùng được. htmx loại bỏ toàn bộ giới hạn đó.
Hypermedia-Driven Application (HDA)
Kiến trúc HDA là cách tiếp cận ngược lại với SPA: server giữ toàn bộ logic và state, trả về HTML fragments qua AJAX. Client chỉ việc swap HTML vào DOM — không cần JSON parsing, không cần state management, không cần client-side routing. Đây chính xác là cách web hoạt động từ đầu, chỉ là mượt mà hơn.
2. Core Attributes — mọi thứ bắt đầu từ HTML
htmx mở rộng HTML bằng một bộ attributes đơn giản nhưng mạnh mẽ. Bạn không cần viết một dòng JavaScript nào — chỉ cần thêm attributes vào HTML elements.
hx-get / hx-post / hx-put / hx-delete
Gửi HTTP request bất kỳ method nào từ bất kỳ element nào — không giới hạn ở <a> hay <form>.
hx-trigger
Chọn event nào sẽ trigger request: click, keyup, load, revealed, every 2s, hoặc custom event.
hx-target
Chỉ định element nào nhận response HTML. Dùng CSS selector: #result, closest tr, next .panel.
hx-swap
Cách swap response vào target: innerHTML, outerHTML, beforeend, afterbegin, delete, none.
hx-indicator
Hiện loading indicator tự động khi request đang xử lý — UX tốt mà không cần state loading.
hx-confirm
Hiện dialog xác nhận trước khi gửi request — lý tưởng cho delete actions.
Ví dụ thực tế: Search trực tiếp
<input type="search" name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-indicator="#spinner"
placeholder="Tìm kiếm...">
<span id="spinner" class="htmx-indicator">Đang tải...</span>
<div id="results"></div>
Chỉ với 5 dòng HTML, bạn có live search với debounce 300ms, loading indicator, và kết quả render tự động — không cần React, không cần useState, không cần useEffect.
Ví dụ: Infinite scroll
<tr hx-get="/contacts?page=2"
hx-trigger="revealed"
hx-swap="afterend">
<td>Loading More...</td>
</tr>
revealed trigger khi element scroll vào viewport — server trả về batch rows tiếp theo kèm row trigger mới cho page 3. Infinite scroll hoàn chỉnh mà không cần IntersectionObserver hay scroll event handler.
3. Kiến trúc Hypermedia vs SPA
graph LR
subgraph SPA["SPA Traditional"]
C1[Client App
React/Vue] -->|"JSON API"| S1[REST/GraphQL Server]
S1 -->|"JSON Data"| C1
C1 -->|"Client Router
State Manager
Virtual DOM"| U1[UI Render]
end
subgraph HDA["Hypermedia htmx"]
C2[HTML + htmx
~14KB] -->|"HTTP Request"| S2[Server]
S2 -->|"HTML Fragment"| C2
C2 -->|"DOM Swap"| U2[UI Update]
end
style SPA fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style HDA fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style C1 fill:#e94560,stroke:#fff,color:#fff
style S1 fill:#2c3e50,stroke:#fff,color:#fff
style U1 fill:#16213e,stroke:#fff,color:#fff
style C2 fill:#4CAF50,stroke:#fff,color:#fff
style S2 fill:#2c3e50,stroke:#fff,color:#fff
style U2 fill:#16213e,stroke:#fff,color:#fff
So sánh kiến trúc SPA vs Hypermedia-Driven Application
| Tiêu chí | SPA (React/Vue) | htmx (Hypermedia) |
|---|---|---|
| Bundle size | 150–500KB+ (framework + deps) | ~14KB (htmx only) |
| Build tooling | Webpack/Vite + transpiler + bundler | Không cần — thêm script tag là xong |
| State management | Redux/Pinia/Zustand + sync logic | Server giữ state — Single Source of Truth |
| Server response | JSON → client parse → render | HTML fragment → swap trực tiếp |
| SEO | Cần SSR/SSG phức tạp | Mặc định SEO-friendly (server render) |
| Offline support | Có thể (Service Worker + cache) | Hạn chế — phụ thuộc server |
| Phù hợp cho | Rich interactive apps, offline-first | CRUD, dashboards, content-heavy sites |
| Learning curve | Cao — JSX, hooks, state, routing, build | Thấp — biết HTML là dùng được |
4. htmx 4.0 — Bước nhảy lớn năm 2026
htmx 4.0 (đang beta, dự kiến ra mắt giữa 2026) mang đến những thay đổi kiến trúc quan trọng nhất kể từ khi htmx ra đời:
4.1. Chuyển từ XHR sang Fetch API
Toàn bộ transport layer được viết lại từ XMLHttpRequest sang Fetch API. Đây không đơn giản là đổi API — nó mở ra khả năng mới hoàn toàn: streaming responses.
// htmx 4.0 tận dụng ReadableStream
// Server gửi HTML fragments dần dần → UI update real-time
// Không cần đợi toàn bộ response hoàn tất
<div hx-get="/dashboard/live"
hx-trigger="load">
<!-- Fragments stream vào khi server xử lý xong từng phần -->
</div>
Streaming UI — Thánh bái của web hiện đại
Với Fetch streaming, htmx 4.0 có thể nhận và render từng phần HTML ngay khi server gửi — tương tự React Server Components streaming nhưng không cần framework phức tạp. Dashboard với 10 widget? Mỗi widget render ngay khi data sẵn sàng, không đợi cả 10 xong hết.
4.2. Idiomorph — DOM merging thông minh
Trước đây Idiomorph là extension, htmx 4.0 đưa nó thành mặc định. Thay vì swap thô (replace toàn bộ innerHTML), Idiomorph so sánh (diff) HTML cũ và mới rồi chỉ update những node thực sự thay đổi — giữ nguyên focus state, scroll position, CSS transitions.
graph TD
A["Server Response
HTML Fragment"] --> B["Idiomorph Diff"]
B --> C{"Node thay đổi?"}
C -->|"Có"| D["Update node đó"]
C -->|"Không"| E["Giữ nguyên"]
D --> F["DOM Updated
Focus & Scroll preserved"]
E --> F
style A fill:#e94560,stroke:#fff,color:#fff
style B fill:#2c3e50,stroke:#fff,color:#fff
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#4CAF50,stroke:#fff,color:#fff
style E fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style F fill:#16213e,stroke:#fff,color:#fff
Idiomorph diff algorithm — chỉ update DOM nodes thay đổi
4.3. <hx-partial> — Multi-target swap
Tag mới <hx-partial> cho phép server response chứa nhiều fragments, mỗi fragment nhắm đến một target khác nhau:
<!-- Server response với multiple partials -->
<hx-partial hx-target="#sidebar">
<nav>Updated sidebar content</nav>
</hx-partial>
<hx-partial hx-target="#notification-count">
<span class="badge">3</span>
</hx-partial>
<hx-partial hx-target="#main-content">
<div>New main content here</div>
</hx-partial>
Một request duy nhất có thể update sidebar, notification badge, và main content cùng lúc — thay thế hoàn toàn Out-of-Band Swaps cũ với syntax rõ ràng hơn.
4.4. Status-Specific Swapping
Xử lý lỗi HTTP trực tiếp trong HTML, không cần JavaScript error handler:
<form hx-post="/api/orders"
hx-target="#result"
hx-status:404="#not-found"
hx-status:422="#validation-errors"
hx-status:5xx="#server-error">
...
</form>
<div id="not-found" style="display:none"></div>
<div id="validation-errors" style="display:none"></div>
<div id="server-error" style="display:none"></div>
4.5. View Transitions API tích hợp sẵn
htmx 4.0 tích hợp mặc định View Transitions API của browser — chuyển trang và swap elements có animation mượt mà mà không cần viết CSS transition thủ công.
4.6. Breaking change: Prop inheritance
Lưu ý khi migrate từ htmx 2.x
Attributes như hx-target không còn tự động inherit từ parent element. Phải dùng modifier :inherited rõ ràng: hx-target:inherited="#div". Thay đổi này cải thiện tính locality — dễ đọc code hơn vì mỗi element tự chứa đủ thông tin.
5. Tích hợp htmx với ASP.NET Core
htmx hoạt động với bất kỳ backend nào có thể trả về HTML — nhưng tích hợp đặc biệt tốt với server-side rendering frameworks như ASP.NET Core Razor Pages/MVC.
5.1. Minimal API endpoint trả HTML fragment
// Program.cs — ASP.NET Core Minimal API
app.MapGet("/search", async (string? q, AppDbContext db) =>
{
var results = await db.Products
.Where(p => string.IsNullOrEmpty(q) || p.Name.Contains(q))
.Take(20)
.ToListAsync();
// Trả về HTML fragment, không phải JSON
var html = string.Join("", results.Select(p =>
$"<tr><td>{p.Name}</td><td>{p.Price:C}</td></tr>"));
return Results.Content(html, "text/html");
});
app.MapDelete("/products/{id}", async (int id, AppDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product is null) return Results.Content("", "text/html");
db.Products.Remove(product);
await db.SaveChangesAsync();
return Results.Content("", "text/html"); // empty = delete row
});
5.2. Razor Pages partial rendering
// Pages/Products/Index.cshtml.cs
public class IndexModel : PageModel
{
public async Task<IActionResult> OnGetSearchAsync(string q)
{
// Detect htmx request
if (Request.Headers.ContainsKey("HX-Request"))
{
Products = await _db.Products
.Where(p => p.Name.Contains(q))
.ToListAsync();
return Partial("_ProductList", Products);
}
return Page(); // Full page for non-htmx
}
}
<!-- _ProductList.cshtml — Razor Partial -->
@model List<Product>
@foreach (var p in Model)
{
<tr>
<td>@p.Name</td>
<td>@p.Price.ToString("C")</td>
<td>
<button hx-delete="/products/@p.Id"
hx-target="closest tr"
hx-swap="outerHTML"
hx-confirm="Xóa sản phẩm này?">
Xóa
</button>
</td>
</tr>
}
5.3. NuGet package hỗ trợ
Package Htmx.TagHelpers cung cấp Tag Helpers cho Razor, giúp viết htmx attributes thuận tiện hơn trong .cshtml. Package Htmx cung cấp extension methods để detect htmx requests và set response headers (HX-Trigger, HX-Redirect, HX-Retarget).
6. Khi nào dùng htmx, khi nào không?
| Rất phù hợp ✅ | Không phù hợp ❌ |
|---|---|
| CRUD applications, admin panels | Ứng dụng offline-first (PWA) |
| Dashboards, reporting, data tables | Drawing tools, game UIs, editors |
| E-commerce product listing, cart | Real-time collaboration (Google Docs) |
| Content-heavy sites, blogs, CMS | Mobile apps (React Native/Flutter) |
| Internal tools, back-office systems | Complex drag-and-drop interfaces |
| Landing pages có form interactions | Apps cần xử lý nhiều trên client |
Kết hợp htmx + Alpine.js
Khi cần một ít client-side logic (toggle menu, dropdown, modal) mà không đủ lý do dùng React — Alpine.js (~17KB) kết hợp htmx là combo hoàn hảo. htmx xử lý server interactions, Alpine.js xử lý UI state thuần client.
7. Performance thực tế
Một case study đáng chú ý trong năm 2026: một enterprise dashboard chuyển từ React sang htmx đã giảm 70% client-side code và đạt 40% improvement về Time-to-Interactive. Nguyên nhân chính:
- Không có hydration cost: SPA frameworks phải download JS → parse → hydrate DOM trước khi interactive. htmx: load 14KB → chạy ngay.
- HTML over the wire nhỏ hơn JSON: Với server-side template (Razor, Jinja, Go templates), HTML fragment thường nhỏ hơn JSON + client-side template logic.
- No client-side routing overhead: Browser xử lý navigation natively, htmx chỉ swap content.
- CDN-cacheable fragments: HTML fragments có thể cache ở CDN layer — JSON API responses thường khó cache hơn.
8. Best Practices
8.1. Server trả về đúng fragment, không full page
Detect header HX-Request: true để biết request đến từ htmx. Nếu có → trả partial HTML. Nếu không → trả full page (progressive enhancement).
8.2. Dùng hx-boost cho progressive enhancement
<body hx-boost="true">
<!-- Mọi link và form tự động AJAX-ified -->
<!-- Fallback: hoạt động bình thường nếu JS bị tắt -->
<a href="/about">About</a>
</body>
hx-boost biến mọi link và form thành AJAX request tự động — trang load nhanh hơn vì chỉ swap <body> thay vì reload cả page. Nếu JavaScript bị tắt, mọi thứ vẫn hoạt động bình thường qua navigation truyền thống.
8.3. Dùng HX-Trigger response header cho events
// Server gửi event sau khi xử lý thành công
Response.Headers.Append("HX-Trigger", "showToast");
// Client lắng nghe event
<div hx-trigger="showToast from:body"
hx-get="/toast/success"
hx-swap="innerHTML"></div>
8.4. Security considerations
- Luôn validate và sanitize input phía server — htmx không thay đổi bề mặt tấn công so với form truyền thống.
- Dùng CSRF tokens cho mọi mutation (POST/PUT/DELETE) — htmx tự động gửi cookie nhưng bạn vẫn cần CSRF protection.
- Cẩn thận với
hx-targettrỏ vào elements quan trọng — đảm bảo server chỉ trả HTML an toàn.
9. Timeline phát triển htmx
Kết luận
htmx không phải là "React killer" — nó là một lựa chọn thay thế hợp lý cho những bài toán mà SPA framework là quá mức cần thiết. Với triết lý hypermedia-driven, kích thước siêu nhẹ, và những cải tiến đáng kể trong version 4.0, htmx chứng minh rằng bạn không cần 300KB JavaScript framework để xây dựng ứng dụng web động và mượt mà.
Nếu bạn đang xây dựng CRUD apps, internal tools, hay content-heavy sites — htmx kết hợp với ASP.NET Core hoặc bất kỳ backend nào là một lựa chọn đáng cân nhắc nghiêm túc.
Tham khảo
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.