So sánh: CSS-in-JS vs CSS module

So sánh: CSS-in-JS vs CSS module
Photo by Laurentiu Iordache from Unsplash

CSS luôn là vấn đề nan giải với các lập trình viên frontend. Việc so sánh CSS module và CSS-in-JS sẽ là một chủ đề phức tạp ở thời điểm hiện tại và trong vài năm tới, đặc biệt là những vấn đề liên quan đến hiệu suất. Tuy nhiên, CSS cũng đang bổ sung rất nhiều tính năng mới (nguồn: web.dev), điều có thể thay đổi rất nhiều yếu tố trong tương lai.

Bài viết này sẽ trình bày sự so sánh giữa CSS-in-JS và CSS module. Nhiều đoạn code tôi sẽ sử dụng React. Nguyên nhân do đây thư viện frontend duy nhất là tôi đã từng làm việc với cả CSS và CSS-in-JS. Các thư viện khác cũng sẽ tương tự (đoán thế 😄).

CSS và vấn đề render blocking

Thiết kế ban đầu của HTML khiến các trình duyệt hoạt động như sau:

  • HTML sẽ được tải đầu tiên và phân tích.
  • Sau đó từ code HTML, các file CSS sẽ được tải.
  • Sau khi tải xong CSS, trình duyệt sẽ xây dựng CSS Object Model (CSSOM) từ toàn bộ CSS (bao gồm cả inline CSS).
  • Sau đó bộ render của trình duyệt sẽ hiển thị trang web cho người dùng.

Cách trình duyệt render một trang web vẫn được duy trì đến tận bây giờ. Chính vì cách hoạt động như vậy, CSS được cho là sẽ block việc render trang web (nguồn: web.dev).

Việc render chậm trễ sẽ ảnh hưởng đến trải nghiệm người dùng. Chậm vài phần nghìn giây cũng đồng nghĩa với việc có thêm nhiều người dùng sẽ từ bỏ dịch vụ. Vì vậy, việc tối ưu hóa CSS vô cùng quan trọng.

Với sự xuất hiện của HTTP/2 và cả HTTP/3, nhiều file HTML, CSS, JS, v.v… có thể được tải xuống cùng một lúc. Điều này sẽ giúp tốc độ tải trang tăng lên, thời gian chờ giảm đi và việc render sẽ diễn ra nhanh chóng hơn.

Tuy nhiên, chỉ tăng tốc độ tải CSS vẫn chưa hoàn toàn giải quyết được vấn đề. Hiệu suất của một trang web còn phụ thuộc vào nhiều yếu tố khác nữa. Bản chất CSS block việc render vẫn không thay đổi. Như những dự án mà tôi từng trải qua, vấn đề của CSS là nó sẽ có rất nhiều code “dư thừa”.

Khi render một trang web, chỉ một phần rất nhỏ CSS được sử dụng, phần lớn CSS được tải về là dư thừa với trang web đó. Có những phần code không bao giờ được sử dụng, hoặc chỉ được sử dụng ở một số trang nhất định. Thế nhưng, mỗi trang web vẫn sẽ phải tải toàn bộ CSS.

Code dư thừa sẽ đặc biệt lớn khi sử dụng những thư viện CSS dựng sẵn, như Bootstrap chẳng hạn. Mặc dù rất tiện lợi trong quá trình phát triển, những thư viện như vậy sẽ có lượng code lớn mà phần lớn trong số đó sẽ không được dùng đến.

Code CSS nhiều hơn, đồng nghĩa với việc xử lý CSSOM mất thời gian, việc block render diễn ra lâu hơn. Để đối phó với tình trạng này, cách làm thông thường là chia nhỏ CSS thành nhiều file. Chỉ giữ lại những code thiết yếu đối với từng trang web sẽ là phương án tối ưu nhất.

Lý thuyết là thế, nhưng việc chia nhỏ CSS thành nhiều file cũng không phải dễ dàng. Trên thực tế, việc chia nhỏ CSS gặp rất nhiều khó khăn liên quan đến việc xác định code chung, code riêng, rồi việc tích hợp preprocessor, v.v…

Rất may, với React, việc chia nhỏ CSS dễ dàng hơn khá nhiều. Trước đây, mỗi component trong React có thể được xây dựng với CSS riêng, kiểu như dưới dây. Bằng cách này, mỗi file CSS sẽ được gắn với component tương ứng và sẽ chỉ được gọi khi component được sử dụng.

...
import "button.css";

export default function Button() {
    ...
}

Nhưng phương pháp này vẫn tồn tại nhiều vấn đề. Với các ứng dụng React, việc có hàng trăm component khác nhau không phải là hiếm. Trong tình trạng đó, việc xung đột khi đặt tên class rất dễ xảy ra. Và vì các class CSS này được dùng cho cho toàn bộ ứng dụng, khi xung đột xảy ra, các thuộc tính có thể được gộp, ghi đè và gây ra lỗi không mong muốn. Debug trong trường hợp này cũng không đơn giản khi cùng một class nhưng lại được viết ở nhiều file khác nhau.

CSS-in-JS

CSS-in-JS được cho là có thể giải quyết vấn đề trên. CSS-in-JS là một lớp chức năng cho phép code CSS trực tiếp trong JavaScript. Nhờ đó, CSS-in-JS mang lại rất nhiều tính năng mới lạ mà CSS không có (ví dụ nhúng các xử lý logic).

Thư viện đầu tiên cho phép code trực tiếp CSS trong JS là JSS. Thư viện này được phát triển từ năm 2015 và đến nay vẫn được bảo trì và có thể tiếp tục sử dụng.

Khi công nghệ ngày càng phát triển hơn, với những thư viện frontend như React, JavaScript giờ đây đóng vai trò quan trọng đối với giao diện một trang web, bao gồm cả việc render. Việc sử dụng CSS-in-JS lại được quan tâm hơn.

Một thư viện CSS-in-JS là styled-components đã xuất hiện. CSS-in-JS giờ đây tiện lợi và dễ dàng hơn rất nhiều. Hiện tại, đây là thư viện phổ biến nhất để sử dụng CSS-in-JS trong React.

Với styled-components, chúng ta có thể xây dựng một component như sau:

import styled from 'styled-components';

const StyledButton = styled.button`
    padding: 0.75em 1em;
    background-color: ${ ({ primary }) => ( primary ? "#07c" : "#333" ) };
    color: white;

    &:hover {
        background-color: #111;
    }
`;

export default StyledButton;

styled-component có thể được import ở bất cứ đâu và xây dựng các component cùng với stylesheet của nó ngay trong code React. Sau đó, các component này có thể được gọi bình thường như mọi component khác mà không cần lo lắng về vấn đề xung đột CSS:

import StyledButton from './components/styles/Button.styled';

function App() {
    return (
        <div className="App">
            ...
            <StyledButton>Default</StyledButton>
            <StyledButton primary={true}>Primary</StyledButton>
        </div>
    );
}

export default App;

Bằng cách sử dụng style-component, CSS áp dụng cho mỗi component là riêng biệt, nên không phải lo lắng về vấn đề xung đột. Lập trình viên cũng không cần phải nghĩ tên để đặt cho các class (việc cũng tốn kha khá công sức), vì chúng sẽ được thư viện sinh tự động. Hơn thế nữa, các thuộc tính CSS lại có thể thêm/bớt hoặc thay đổi tùy vào props hay state của từng component.

Ưu điểm của CSS-in-JS

Cá nhân tôi thực sự thích khi làm việc với CSS-in-JS, cụ thể là với styled-component. Dưới dây là những ưu điểm nổi bật của nó.

Không lo xung đột

Mọi CSS được đóng gói trong component và lập trình viên không cần lo về chuyện xung đột với các component khác. Việc suy nghĩ để đặt tên cho các class cũng không cần thiết, vì tên class sẽ được sinh tự động.

CSS thay đổi linh hoạt

Đây cũng là một trong những ưu điểm rất lớn cũng như tính năng tuyệt vời của CSS-in-JS. Như ví dụ ở phần trước, CSS hoàn toàn có thể thay đổi tùy theo giá trị của props hay state. Nhờ tính năng này, việc thay đổi giao diện một component trở nên rất đơn giản.

Nhờ khả năng thay đổi CSS linh hoạt mà việc xây dựng các component responsive thay đổi theo kích thước màn hình hay thay đổi tùy theo thao tác của người dùng cũng đơn giản hơn nhiều so với code CSS truyền thống. Ngoài ra, việc một component cần tùy biến cho phù hợp với từng trang cụ thể rất dễ xảy ra. Vì vậy, việc CSS có thể thay đổi linh hoạt thông qua props cũng giúp công việc của lập trình viên đỡ vất vả hơn khá nhiều.

Đặc biệt, nếu kết hợp styled-component và styled-system thì sự linh hoạt của CSS-in-JS còn tăng lên gấp nhiều lần. Hầu như mọi thuộc tính CSS chỉ cần truyền props là xong 👍.

Không cần selector phức tạp

CSS-in-JS cho phép chúng ta code CSS cho từng component cụ thể. Do đó, những CSS selector phức tạp hoàn toàn không cần thiết. Điều này còn mang đến một ưu điểm khác là lập trình viên không cần phải lo lắng về mức độ xác định của selector, vốn là thứ rất khó debug mỗi khi có lỗi về giao diện. Những thứ dễ gây khó khăn cho công việc frontend như !important cũng không cần dùng đến.

Dễ dàng maintain

Việc maintain một dự án sử dụng CSS-in-JS đơn giản hơn nhiều so với việc sử dụng CSS độc lập. Bởi vì tất cả, kể cả CSS, cũng đều được quản lý trong 1 file. Từng thẻ HTML sẽ có CSS gắn liền với nó, nên việc tìm và đọc code không quá khó khăn. Với những IDE hay editor hiện đại, lập trình viên còn có thể nhanh chóng “nhảy” đến đúng đoạn code tương ứng chỉ bằng vài phím tắt.

Ngoài ra, CSS sẽ được đóng gói trong component, do đó, có thể phòng tránh code thừa. Việc thay đổi code cũng dễ dàng mà không lo các thành phần khác bị ảnh hưởng. Khi làm việc nhóm, những tình huống kiểu như có những dòng CSS không ai dám xóa vì không dám chắc nó còn được sử dụng ở đâu cũng không xảy ra.

Nhược điểm của CSS-in-JS

Mặc dù mang lại rất nhiều ưu điểm. Nhưng CSS-in-JS không giải quyết được vấn đề lớn nhất là hiệu suất của trang web. Không những vậy, CSS-in-JS cũng có những hạn chế của riêng nó.

Cần thêm thư viện

Để có thể sử dụng CSS-in-JS, cần cài đặt thêm các thư viện tương ứng. Rất nhiều package khác nhau sẽ được cài đặt và thực thi để có thể mang lại những tính năng cực kỳ mạnh mẽ ở trên.

Cần thêm thư viện đồng nghĩa với việc file bundle sẽ lớn hơn, JavaScript cũng phức tạp hơn. Điều này phần nào cũng ảnh hưởng đến hiệu suất trang web.

Render chậm hơn

CSS-in-JS cần phải thực thi JavaScript trước để phân tích stylesheet cho các component và sau đó CSS đó mới được đưa vào trang web. Nhiều bước xử lý hơn đồng nghĩa với việc render sẽ chậm hơn.

Mặc dù một số code CSS sẽ được trích xuất trước trong quá trình build cho production, nhưng số này vẫn còn quá ít. Do tính chất linh hoạt của nó, trang web vẫn phụ thuộc vào xử lý JavaScript để render stylesheet của các component.

Khó dùng cache

Cache các file tĩnh như CSS là một trong những phương pháp hiệu quá để tăng hiệu suất trang web. Tuy nhiên, với CSS-in-JS, vì phần lớn code CSS được nhúng vào JavaScript, mọi chuyện không đơn giản như thế. Ngoài ra, các class được sinh tự động và sẽ thay đổi liên tục mỗi khi code thay đổi cũng làm cache phức tạp hơn.

Hiệu suất thấp

CSS-in-JS được cho là chỉ phù hợp với những ứng dụng nhỏ (khi mà ảnh hưởng của CSS đến hiệu suất không lớn) hoặc những ứng dụng không quan trọng hiệu suất. Với những ứng dụng lớn, nhiều thành phần phức tạp, hiệu suất của CSS-in-JS là vấn đề lớn của nó.

Tôi đã từng phải tối ưu hiệu suất một ứng dụng React sử dụng CSS-in-JS (styled-component), tôi đã bỏ cuộc và chuyển sang dùng CSS module (sẽ trình bày ở phần sau).

Các file bundle JS, CSS đều có kích thước quá lớn. Vì CSS được sinh tự động, rất nhiều class có cùng thuộc tính sẽ được sinh ra. Cũng vì CSS được sinh tự động, rất khó tối ưu code CSS.

Dưới đây là hiệu suất của ứng dụng (do bằng light house) mà tôi đã tối ưu khi sử dụng CSS-in-JS và sau khi chuyển sang dùng module CSS.

CSS-in-JS

FCP: 2.2
SI: 9.1
LCP: 13.1
TTI: 13.2

CSS module

FCP: 2.1
SI: 7.6
LCP: 10
TTI: 10.5

Với code gần như giống nhau, file bundle khác nhau rất nhiều và sự chênh lệch về hiệu suất là rất đáng kể.

DOM phình to

CSS-in-JS hoạt động bằng cách phân tích code bởi JavaScript, sau đó trích xuất CSS và đẩy vào HTML bằng thẻ style. Với những ứng dụng phức tạp, việc có hàng trăm thẻ style khác nhau cũng có thể xảy ra. Điều đó khiến DOM bị phình to một cách không cần thiết.

Mất thời gian để làm quen

Những CSS preprocessor mà người làm frontend thường dùng như SCSS, PostCSS, v.v… không thể sử dụng được với CSS-in-JS, ít nhất là thời điểm hiện tại.

Không chỉ vậy, CSS-in-JS không hoàn toàn là CSS nên lập trình viên sẽ mất thời gian để học và làm quen với cú pháp mới. Nhiều tính năng của CSS cũng không được hỗ trợ nên những người đã quen với CSS hay SCSS sẽ gặp ít nhiều vất vả trong thời gian ban đầu.

Giải pháp thay thế: CSS Module

Hiện tại, các file CSS cũng có thể được sử dụng như một module và được import vào file JavaScript như các module khác. Mặc định, CSS module sẽ được đóng gói trong component mà nó được import. Vì vậy, cho dù cùng tên class xuất hiện ở file khác nhau vẫn không xảy ra xung đột.

Dưới đây là một đoạn code sử dụng CSS module:

import styles from './Button.module.css';

export default function Button(props) {
    return (
        <a
            href={props.href ? props.href : '#'}
            className={styles.btn}
        >
            {props.name}
        </a>
    );
}

Ưu điểm CSS module

Giải quyết vấn đề xung đột

Ưu điểm lớn nhất của CSS module là nó giải quyết được vấn đề xung đột. Khi đã giải quyết được vấn đề này, CSS-in-JS có thể trở thành giải pháp hơi thừa cho một vấn đề đơn giản.

Như trong đoạn code ví dụ ở trên, CSS module có thể giải quyết được vấn đề xung đột trong khi vẫn sử dụng CSS, SCSS như thông thường.

Cache dễ dàng

Dùng CSS module cũng tương tự như việc dùng CSS truyền thống. Các file bundle CSS hoàn toàn có thể được cache để tăng hiệu suất trang web.

Có thể dùng CSS preprocessor

Những công cụ như SCSS, PostCSS, v.v… hoàn toàn có thể được sử dụng mà không gặp phải khó khăn nào.

Không cần làm quen

Việc sử dụng CSS module hoàn toàn giống như CSS. Vì vậy, lập trình viên không cần thời gian làm quen và có thể làm việc ngay.

Có thể áp dụng ngay và luôn

Không cần thêm thư viện nào để có thể làm việc với CSS module. Những module bundler hiện tại đều đã hỗ trợ CSS module.

Nhược điểm CSS Module

Mặc dù có nhiều ưu điểm, CSS module không phải là giải pháp hoàn hảo. Nó cũng có những vấn đề của riêng mình.

CSS không linh hoạt

Với CSS module, mọi code CSS được code trong file riêng. Do đó, nó không có khả năng thay đổi linh hoạt như CSS-in-JS. Không thể sử dụng JS hay nhúng bất cứ logic nào vào CSS.

Maintain khó hơn

Bắt buộc phải code CSS trong file riêng để dùng CSS module. Việc có thêm file trong nhiều trường hợp có thể làm khó quá trình debug cũng như maintain. Hiện tại, editor mà tôi sử dụng để code cũng không hỗ trợ việc nhảy đến code tương ứng trên file CSS. Vì thế, để tìm và đọc code cũng cần nhiều thao tác thủ công hơn.

Khi nào nên dùng CSS Module

CSS module là lựa chọn hợp lý nếu xây dựng những ứng dụng cần hiệu suất cao. Sử dụng CSS module tương tự như CSS truyền thống, khiến nó dễ dàng sử dụng cũng như tối ưu hóa.

CSS module cũng dễ dàng áp dụng các thư viện CSS dựng sẵn. Chỉ cần một chút hiểu biết về CSS là có thể sử dụng được.

Kết luận

Trong bài viết này, tôi đã trình bày những so sánh giữa CSS-in-JS và CSS module trong việc xây dựng các ứng dụng React. Những người thành thạo JavaScript có thể yêu thích CSS-in-JS hơn còn những người đã quen với CSS hay SCSS sẽ muốn sử dụng CSS module.

Mỗi công cụ có những đặc điểm riêng, tùy vào ứng dụng cụ thể mà lập trình viên có thể lựa chọn công cụ nào phù hợp.

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.