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

~14KB Kích thước gzip — nhỏ hơn React 20 lần
47K+ GitHub Stars
0 Dependencies — không phụ thuộc bất kỳ thư viện nào
0 Build step — không cần bundler, transpiler

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><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:

-70% Client-side code sau khi migrate
-40% Time-to-Interactive cải thiện
0 Hydration cost — không có JS framework nào cần hydrate
  • 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-target trỏ vào elements quan trọng — đảm bảo server chỉ trả HTML an toàn.

9. Timeline phát triển htmx

2013
intercooler.js — tiền thân của htmx, do Carson Gross phát triển.
2020
htmx 1.0 ra mắt — viết lại intercooler.js, bỏ dependency jQuery, nhỏ gọn hơn.
2024
htmx 2.0 — bỏ support IE, cleanup API, thêm head merging và response validation.
2026 Q1
htmx 4.0 Beta — chuyển từ XHR sang Fetch API, Idiomorph mặc định, <hx-partial>, View Transitions. Bỏ qua version 3 với lý do "đã hứa không có htmx 3.0".
2026 Q3 (dự kiến)
htmx 4.0 Stable — ra mắt chính thức vào mùa hè 2026.

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