· tailwind / css-modules / css

Tailwind CSS vs CSS Modules — chi phí dài hạn thực tế

Tailwind thắng năm đầu. Chi phí dài hạn gần như chỉ phụ thuộc vào một quyết định đưa ra sớm. Đây là những gì thực sự tích lũy qua 1–3 năm trên codebase thực tế.

Bởi Ethan

2.617 từ · 14 phút đọc

Tailwind thắng năm đầu. Ít file hơn, vòng lặp nhanh hơn, không phải đặt tên class. Đến năm hai, hóa đơn đến — nhưng nó lớn hay nhỏ phụ thuộc gần như hoàn toàn vào một quyết định đưa ra sớm: bạn đã kết nối Tailwind với semantic design tokens ngay từ đầu, hay để developer tự ý dùng bg-blue-500 trực tiếp.

CSS Modules thì ngược lại. Chi phí ban đầu cao hơn (hai file mỗi component, naming conventions cần kiểm soát), nhưng lộ trình bảo trì dài hạn dễ dự đoán hơn. Bạn luôn biết chính xác style nằm ở đâu.

Không công cụ nào rẻ hơn một cách khách quan. Lựa chọn này là đặt cược vào kỷ luật của team bạn và hướng phát triển của codebase.

Bài này dành cho ai

Frontend developer đang chọn chiến lược styling cho một production app mới, hoặc các team đang cân nhắc có nên migrate hay không. Bài viết bao gồm Tailwind CSS v4 (ra mắt tháng 1 năm 2025) và CSS Modules trong Next.js 15+. Nếu bạn đang đánh giá CSS-in-JS như styled-components hay Emotion, bài này không so sánh những thứ đó. Để tìm hiểu về lựa chọn meta-framework thường đi kèm với quyết định styling, xem Next.js vs AstroNext.js vs Remix.

Chúng tôi so sánh gì

  • Tailwind CSS v4.0 — Oxide engine (viết bằng Rust), @theme blocks, JIT purge. Benchmark chính thức từ dự án Catalyst qua tailwindcss.com/blog/tailwindcss-v4.
  • CSS Modules — CSS Modules spec + typescript-plugin-css-modules hỗ trợ IDE + typed-css-modules kiểm tra lúc compile. Next.js hỗ trợ ngay từ đầu, không cần cấu hình.
  • Dữ liệu migration từ case study auslake.vercel.app (CSS Modules → Tailwind, component library production thực tế).
  • Dữ liệu adoption từ State of CSS 2024 (9.704 người tham gia khảo sát).

Bundle size: Tailwind thắng CSS, HTML trả phần còn lại

Cơ chế JIT/purge của Tailwind v4 chỉ emit các class thực sự xuất hiện trong source. Một project Next.js điển hình đạt 6–12 KB sau khi gzip. Case study auslake cho thấy mức giảm CSS 97%: 900 KB từ CSS Modules co xuống còn 30 KB với Tailwind, xóa hơn 3.000 dòng CSS.

Con số đó có thật. Nhưng chưa đầy đủ.

Một phân tích production đã nói thẳng phần còn lại của câu chuyện:

“CSS từ 45 KB xuống còn 8 KB với Tailwind. Tuyệt! Nhưng HTML từ 120 KB tăng lên 340 KB. Tổng cộng tăng thêm: 183 KB.”

CSS là tài nguyên có thể cache và dùng chung cho nhiều route. Người dùng vào hai trang trên site của bạn chỉ tải CSS một lần. HTML thì không được cache giữa các trang. Một component Tailwind với 15–20 utility class trên mỗi phần tử chuyển payload từ phía cache-friendly sang phía không thể cache — ở mỗi trang, cho mỗi người dùng.

CSS Modules không có cơ chế purge tích hợp sẵn, nhưng Next.js App Router tự động chia nhỏ CSS theo route. Các app phức tạp với nhiều route riêng biệt có thể có hành vi tải resource chính xác hơn so với một file CSS Tailwind gốc dùng chung.

Kết luận: Tailwind thắng về kích thước file CSS thuần. Ở mật độ component cao, chi phí byte HTML bù một phần cho lợi thế đó. Khoảng cách có thật nhưng nhỏ hơn con số 97% cho thấy.

Tốc độ build

Benchmark chính thức của Tailwind v4 từ dự án Catalyst:

Metricv3.4v4.0Tăng
Full build378ms100msNhanh hơn 3.78×
Incremental (có CSS mới)44ms5msNhanh hơn 8.8×
Incremental (không có CSS mới)35ms192µsNhanh hơn 182×

CSS Modules không có bước compile riêng — bundler xử lý chúng song song với các file component. Với hầu hết team, chênh lệch tốc độ build không phải yếu tố quyết định.

Chi phí refactor: nơi con đường dài hạn rẽ ra

Đây là chỗ khoảng cách chi phí dài hạn thực sự ẩn náu.

Tailwind: hai chế độ

Refactor theo token (con đường thuận lợi): Bạn thay đổi colors.brand trong tailwind.config.js hoặc block @theme trong v4. Mọi component dùng bg-brand nhận thay đổi ngay. Không cần sửa file nào. Thật sự rẻ.

Refactor theo class (con đường khó): Một button dùng bg-blue-500 trực tiếp. Bạn cần đổi thành bg-indigo-600. Giờ bạn phải grep qua tất cả file hardcode class đó. Với hàng trăm component, việc này mất công và dễ sai. Các team bỏ qua token layer và dùng arbitrary values (text-[#3B82F6]) phải chịu toàn bộ chi phí refactor. Đây là nguồn gốc phổ biến nhất của phàn nàn “Tailwind rối loạn trong codebase lớn”, và đó là vấn đề kỷ luật, không phải vấn đề công cụ.

Migration của Netlify với ~40 component và team 10 người mất khoảng 4 tuần từ đầu đến cuối. Họ triển khai một utility tùy chỉnh (ctl()) để quản lý component có hơn 5 class nối chuỗi và dùng visual regression testing xuyên suốt.

CSS Modules: rõ ràng, nhưng cần kỷ luật đặt tên

Nguyên tắc co-location làm cho refactor CSS Modules dễ dự đoán. import styles from './Button.module.css' làm dependency tường minh. Để tìm thứ cần thay đổi, chỉ cần mở một file.

Cảnh báo: nếu không có naming conventions được thực thi, CSS Modules tích lũy sự không nhất quán — primaryButton, blueWhiteBtn, btnPrimary rải rác trong team. Case study auslake đã dẫn chính vấn đề này là lý do chính để migrate.

CSS custom properties (CSS variables) phần lớn giải quyết vấn đề global token cho CSS Modules. Định nghĩa --spacing-md: 16px một lần trong file gốc; tham chiếu ở khắp nơi; cập nhật một dòng và mọi component phản ánh thay đổi.

Kết luận: CSS Modules cho phép refactor an toàn và cục bộ hơn khi naming conventions được thực thi. Tailwind cho phép cập nhật global token rẻ hơn khi semantic tokens được dùng từ đầu. Cả hai đều xuống cấp khi kỷ luật mất đi, theo những cách khác nhau.

TypeScript safety: lợi thế ít được nhắc đến

Hầu hết bài so sánh bỏ qua phần này. Nhưng nó quan trọng trong codebase lớn.

Tailwind IntelliSense

Extension VS Code chính thức tailwindcss-intellisense cung cấp autocomplete, hover documentation, và linting cho class không hợp lệ. Nó rút ngắn đáng kể đường cong học tập.

Nhưng nó không làm được: bắt lỗi đánh máy ở compile time. Một text-smm viết sai không tạo style nào và không báo lỗi. TypeScript không kiểm tra tên class dạng string.

CSS Modules với TypeScript

Có ba lựa chọn, sắp xếp theo mức độ phổ biến:

  1. typescript-plugin-css-modules (~692K lượt tải npm mỗi tuần): TypeScript language service plugin — autocomplete IDE và type errors cho .module.css imports. Hạn chế quan trọng: language service plugins không chạy trong tsc, nên CI type-checking không bắt được lỗi CSS class.

  2. typed-css-modules (~186K lượt tải npm mỗi tuần): CLI tool tạo file .d.ts song song với các file .module.css. Hoạt động với tsc. Bắt styles.heroCrad là lỗi compile. Chi phí: thêm file .d.ts trong workspace và một bước generation trong build pipeline.

Khi typed-css-modules được tích hợp vào CI, tên class viết sai sẽ làm build thất bại. Tailwind không thể đảm bảo điều này vì tên class là plain string — không có type system nào có thể validate chúng.

Kết luận: CSS Modules với typed-css-modules bắt được lỗi tên class mà Tailwind không thể. Với các team mà CSS regression là rủi ro thực sự — team lớn, người đóng góp luân chuyển thường xuyên, design system phức tạp — đây là lợi thế an toàn dài hạn có ý nghĩa.

Theming và dark mode

Tailwind

Block @theme của Tailwind v4 định nghĩa tokens vừa là CSS custom properties vừa là utility classes cùng lúc:

@theme {
  --color-brand: oklch(0.6 0.2 250);
  --color-brand-dark: oklch(0.4 0.2 250);
}

Điều này tạo ra cả bg-brand là utility class lẫn var(--color-brand) là CSS variable. Khoảng cách giữa Tailwind và CSS Modules đã thu hẹp đáng kể ở đây.

Pattern dark: prefix (ví dụ: dark:bg-gray-800) hoạt động tốt cho hệ thống 2 theme nhưng trở nên dài dòng khi scale. Các site có nhiều variant dark mode trên từng phần tử sẽ có markup kiểu:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 ...">

Pattern hiện đại được khuyến nghị dùng semantic tokens với class-based dark mode:

@custom-variant dark (&:where(.dark, .dark *));

Cách này loại bỏ hầu hết tình trạng dark: proliferation, nhưng đòi hỏi kiến trúc token từ trước. Team bỏ qua bước này và dùng dark: cho từng phần tử sẽ phải chịu chi phí refactor đáng kể nếu muốn thêm theme thứ ba.

CSS Modules

CSS custom properties trong file .module.css tách rời theme hoàn toàn khỏi markup:

.card {
  background: var(--color-surface);
  color: var(--color-text-primary);
}

Định nghĩa giá trị token một lần trong block :root toàn cục (và một override .dark hoặc block @media (prefers-color-scheme: dark)). Mọi component nhận thay đổi mà không cần sửa markup. Thêm theme thứ ba chỉ cần thêm một file CSS.

Kết luận: Với hệ thống 2 theme (light/dark), @theme của Tailwind v4 đã cạnh tranh được. Với design system doanh nghiệp đa theme có 5+ theme, CSS Modules với CSS custom properties scale tốt hơn vì token layer hoàn toàn tách khỏi component markup.

Onboarding team

Tailwind yêu cầu học hơn 200 tên utility class cùng hệ thống prefix responsive (sm:, md:) và state (hover:, focus:). IntelliSense giúp việc này dễ chịu hơn — developer mới có thể làm việc hiệu quả trong vài ngày với các pattern thông thường. Các tổ hợp responsive + dark + animation phức tạp mất thời gian hơn để thành thạo. Class soup trên các component phức tạp vẫn là rào cản đọc hiểu dù đã onboard đầy đủ.

CSS Modules dùng CSS chuẩn. Bất kỳ developer nào biết CSS đều có thể đọc file .module.css ngay lập tức. Chi phí ban đầu chuyển sang naming conventions (BEM, camelCase — bạn tự chọn) thay vì một cú pháp mới. Kiến thức này áp dụng được cho bất kỳ dự án nào — đây là lợi thế dài hạn thường bị đánh giá thấp khi junior engineer luân chuyển qua codebase.

State of CSS 2024 (9.704 người tham gia khảo sát): ~75% người dùng framework dùng Tailwind; ~61% dùng CSS Modules. Một tỷ lệ đáng kể dùng cả hai đồng thời — Tailwind cho layout, spacing, và typography; CSS Modules cho các style phức tạp dành riêng cho component.

Bảng so sánh

Tiêu chíTailwind CSS v4CSS Modules
Kích thước CSS bundle6–12 KB gzipped (JIT purge)Route-split theo component; không có purge tích hợp
Chi phí byte HTMLCao hơn (15–20+ class mỗi phần tử)Thấp hơn (một tên class)
Tốc độ full build100ms (Oxide engine v4)Không có bước compile riêng
Refactor thay đổi tokenMiễn phí (nếu tokens trong @theme)Miễn phí (nếu dùng CSS custom properties)
Refactor đổi tên classGrep và thay thế trong markupSửa một file .module.css
TypeScript safety lúc compileKhông có (string không được type-check)Có, với typed-css-modules + CI
Dark mode (2 theme)@theme + variant dark: — cạnh tranh đượcCSS variables + một override block
Multi-theme (5+ theme)Phức tạp hơnCSS variables — scale tự nhiên
Setup với Next.jsCần cấu hình PostCSSZero config, native support
Khả năng đọcMarkup nhiều class; ý nghĩa bị ẩnTên có ngữ nghĩa; ý nghĩa rõ ràng
File mỗi component12 (.tsx + .module.css)
Onboarding200+ tên class; IntelliSense hỗ trợCSS chuẩn; đọc được ngay
Adoption (2024)~75% người dùng framework~61% người dùng framework

Một lựa chọn thứ ba đáng biết

StyleX (Meta, open-source) đáng được nhắc đến ở đây. Nó được dùng bởi facebook.com và react.dev, tạo atomic CSS lúc build time (output tương tự Tailwind), và biểu diễn style bằng TypeScript object thay vì tên class dạng string. Một property viết sai là TypeScript error, không phải lỗi thầm lặng. Nó giải quyết đồng thời điểm yếu chính của cả hai công cụ: thiếu safety lúc compile của Tailwind và tình trạng file proliferation của CSS Modules. Nếu bạn đang bắt đầu một design system lớn từ đầu, nó xứng đáng được đánh giá. Không có quan hệ affiliate — nó thuộc về cuộc thảo luận này.

Verdict

Chọn Tailwind nếu: Team bạn nhỏ (≤5 frontend engineer), bạn muốn tốc độ iteration nhanh, và cam kết dùng semantic tokens từ đầu. Lợi thế DX là thực tế. Chi phí bảo trì có thể kiểm soát được khi kỷ luật token được giữ vững.

Chọn CSS Modules nếu: Team bạn có người đóng góp luân chuyển thường xuyên, bạn cần compile-time class-name safety trong CI, hoặc bạn vận hành design system đa theme. Chi phí ban đầu cao hơn sẽ được bù đắp bằng các refactor dễ dự đoán và cục bộ hơn.

Chọn cả hai nếu: Bạn đang xây một Next.js app — đây cũng là những gì Next.js chính thức khuyến nghị — Tailwind cho layout, spacing, và typography; CSS Modules cho component phức tạp mà utility class trở nên khó đọc. Đây là lựa chọn thực tế nhất cho hầu hết team cỡ vừa.

Quyết định tốn kém nhất không phải là bạn chọn công cụ nào. Mà là chọn Tailwind rồi để developer bỏ qua token layer, hoặc chọn CSS Modules rồi bỏ qua naming conventions. Cả hai công cụ đều thất bại theo cùng một cách: để các shortcut dễ dàng tích lũy lại.

Lưu ý

  • Benchmark tốc độ build đến từ blog release Tailwind v4 chính thức (dự án Catalyst). Kết quả của bạn sẽ khác tùy theo kích thước codebase và máy tính.
  • Con số HTML bloat đến từ một case study trên dev.to, không phải đo đạc của chúng tôi.
  • Timeline migration (Netlify, auslake) phản ánh các codebase và team cụ thể; chi phí migration của bạn sẽ khác.
  • Lợi thế TypeScript safety của CSS Modules cần được thiết lập. Nếu không tích hợp typed-css-modules vào CI, bạn chỉ có IDE hints — không an toàn hơn Tailwind IntelliSense đáng kể.

Tài liệu tham khảo

  1. Blog release Tailwind CSS v4 — build benchmarks, Oxide engine, cú pháp @theme: tailwindcss.com/blog/tailwindcss-v4
  2. Tailwind dark mode docs: tailwindcss.com/docs/dark-mode
  3. Next.js CSS documentation — hướng dẫn chính thức, RSC compatibility: nextjs.org/docs/app/getting-started/css
  4. CSS Modules GitHub specification: github.com/css-modules/css-modules
  5. State of CSS 2024 — tools adoption: 2024.stateofcss.com/en-US/tools
  6. Case study migration CSS Modules → Tailwind (auslake.vercel.app) — 900 KB→30 KB, xóa 3K dòng: auslake.vercel.app/blog/migration-tailwindcss
  7. Migration Netlify semantic CSS → Tailwind: netlify.com/blog — From Semantic CSS to Tailwind
  8. Phân tích HTML bloat — CSS 45 KB→8 KB, HTML 120 KB→340 KB: dev.to — Tailwind CSS Won the War But We’re the Losers
  9. typescript-plugin-css-modules npm: npmjs.com/package/typescript-plugin-css-modules
  10. typed-css-modules npm: npmjs.com/package/typed-css-modules