Migrate từ Ant Design sang Shadcn UI: Lộ trình chuyển đổi không làm vỡ sản phẩm đang chạy
Quyết định thay thư viện giao diện của một sản phẩm đang chạy là một trong những việc dễ bị đánh giá thấp nhất trong nghề. Nó không khó về kỹ thuật, nó khó vì khối lượng công việc trải đều trên hàng trăm file và không mang lại tính năng mới nào cho người dùng cuối, nên rất dễ bị bỏ dở giữa chừng, để lại dự án mang cả hai thư viện cùng lúc, tệ hơn hẳn lúc ban đầu.
Bài viết này đưa ra lộ trình cụ thể để việc chuyển đổi kết thúc được. Nếu bạn vẫn đang ở giai đoạn cân nhắc chứ chưa quyết, hãy đọc bài so sánh Shadcn UI với Ant Design v5 trước, còn nếu chưa quen mô hình chép mã nguồn của Shadcn UI thì đọc bài Shadcn UI là gì và cách cài đặt vào Next.js.
1. Bảng quyết định: khi nào việc chuyển đổi là xứng đáng
Trước khi viết dòng mã đầu tiên, hãy trả lời trung thực bảng dưới đây. Nếu phần lớn dự án của bạn rơi vào cột bên phải, hãy dừng lại và giữ nguyên hiện trạng.
| Tình huống | Nên chuyển | Nên giữ Ant Design |
|---|---|---|
| Loại sản phẩm | Trang bán hàng, landing page, phần mềm dịch vụ hướng người dùng cuối | Trang quản trị nội bộ, hệ thống ERP, CRM, công cụ vận hành |
| Yêu cầu thiết kế | Bản sắc riêng, giao diện khác biệt với đối thủ | Chỉ cần đúng chức năng, dùng giao diện mặc định là chấp nhận được |
| Ràng buộc hiệu năng | Có mục tiêu Core Web Vitals rõ ràng, có SEO, người dùng ở mạng chậm | Người dùng đăng nhập bằng máy tính văn phòng, mạng nội bộ |
| Mật độ component nghiệp vụ nặng | Chủ yếu form, danh sách, thẻ nội dung, hộp thoại | Nhiều Transfer, Cascader, Tree kéo thả, Table gộp ô cố định cột |
| Năng lực đội ngũ với Tailwind CSS | Đã thạo, đã có hệ thiết kế bằng token | Chưa từng dùng, quen viết CSS module hoặc styled-components |
| Quỹ thời gian | Có thể chia nhỏ ra nhiều đợt trong 2 tới 4 tháng | Chỉ có một tuần rảnh, sau đó cả đội quay lại chạy tính năng |
Kinh nghiệm thực tế: lý do chính đáng nhất để chuyển là yêu cầu về bản sắc thiết kế chứ không phải dung lượng gói. Nếu chỉ muốn giảm dung lượng, có những cách rẻ hơn nhiều so với thay cả thư viện, xem mục 3.
2. Đo trước khi đổi: con số thật của dự án bạn
Đừng dựa vào con số nghe được trên mạng. Hãy đo chính dự án của mình:
# Cài công cụ phân tích gói cho Next.js
npm install --save-dev @next/bundle-analyzer
# Chạy dựng bản phát hành kèm phân tích
ANALYZE=true npm run build
Ba con số cần ghi lại trước khi động vào bất cứ thứ gì:
- Dung lượng JavaScript của trang nặng nhất sau khi nén, và phần đóng góp của thư viện giao diện trong đó.
- Thời điểm hiển thị nội dung lớn nhất đo bằng công cụ đo hiệu năng ở chế độ giả lập mạng chậm, không phải đo trên máy phát triển.
- Số component đang dùng thật sự. Chạy lệnh tìm kiếm để đếm, con số này thường nhỏ hơn nhiều so với cảm giác.
# Liệt kê chính xác các component antd đang được import
grep -rhoE "import \{[^}]+\} from 'antd'" src | tr ',' '\n' | tr -d " {}" | sort -u
Rất nhiều đội sau khi chạy lệnh này phát hiện cả dự án chỉ dùng khoảng mười tới mười lăm component. Khi đó khối lượng chuyển đổi nhỏ hơn nhiều so với dự đoán ban đầu, và kế hoạch trở nên khả thi.
3. Ba phương án rẻ hơn cần loại trừ trước
Nếu mục tiêu duy nhất là tốc độ, hãy thử ba việc này trước, chi phí thấp hơn hàng chục lần:
| Phương án | Việc phải làm | Hiệu quả thường thấy |
|---|---|---|
| Nạp động component nặng | Dùng cơ chế nhập khẩu động của Next.js cho các màn hình chứa bảng lớn, biểu đồ, trình soạn thảo | Giảm mạnh dung lượng của trang đầu tiên mà không đụng vào thư viện |
| Tinh giản CSS thừa | Rà lại cấu hình quét lớp và loại bỏ CSS không dùng, xem bài Critical CSS Inlining và Unused CSS Purging | Rút ngắn thời gian chặn hiển thị |
| Trích xuất kiểu dáng ở phía máy chủ | Ant Design v5 dùng cơ chế sinh CSS lúc chạy, cần cấu hình trích xuất trước để tránh nhấp nháy giao diện và giảm việc tính toán ở trình duyệt | Cải thiện rõ chỉ số hiển thị nội dung lớn nhất trên App Router |
Chỉ khi ba phương án trên đã làm hết mà vẫn không đạt mục tiêu, hoặc khi lý do chuyển đổi là thiết kế chứ không phải tốc độ, thì mới bước sang phần còn lại của bài viết.
4. Mô hình Strangler Fig: siết dần chứ không viết lại
Viết lại toàn bộ giao diện trong một nhánh riêng rồi gộp một lần là công thức thất bại kinh điển: nhánh đó sống hàng tháng trời, xung đột chồng chất với nhánh chính, và cuối cùng bị bỏ. Cách làm đúng là siết dần theo từng lớp:
- Đóng băng chiều mở rộng. Ra quy ước bằng văn bản: từ hôm nay mọi màn hình mới đều dùng Shadcn UI, không thêm import mới từ thư viện cũ. Có thể ép bằng luật kiểm tra mã nguồn để chặn tự động.
- Chuyển các component lá trước. Nút bấm, ô nhập, thẻ, nhãn, tooltip. Đây là nhóm dùng nhiều nhất và rủi ro thấp nhất.
- Chuyển lớp phủ. Hộp thoại, ngăn kéo, thông báo, hộp xác nhận. Nhóm này ít về số lượng nhưng ảnh hưởng lớn tới cảm nhận.
- Chuyển biểu mẫu theo từng màn hình. Đây là phần tốn công nhất, làm từng màn hình một và phát hành ngay, không gom lô.
- Chuyển bảng dữ liệu sau cùng. Nhóm này khó nhất, để cuối để có thời gian tích lũy kinh nghiệm.
- Gỡ thư viện cũ và khóa cửa. Xóa khỏi tệp phụ thuộc, chạy lại phân tích gói để xác nhận con số thay đổi.
Điểm mấu chốt: mỗi bước đều kết thúc bằng một lần phát hành lên môi trường thật. Không có nhánh nào sống quá một tuần.
5. Bảng ánh xạ component: từ Ant Design sang Shadcn UI
| Ant Design | Tương đương | Mức độ khó |
|---|---|---|
| Button | Button | Dễ, chỉ đổi tên thuộc tính type thành variant |
| Input, Input.TextArea | Input, Textarea | Dễ |
| Modal | Dialog | Dễ về giao diện, nhưng đổi tư duy: Ant Design gọi hàm mệnh lệnh, Radix điều khiển bằng trạng thái |
| Drawer | Sheet | Dễ |
| Popconfirm | AlertDialog | Dễ |
| message, notification | sonner | Dễ, cả hai đều gọi bằng hàm |
| Select | Select | Trung bình, tính năng tìm kiếm trong danh sách phải tự ghép bằng Combobox |
| AutoComplete | Combobox ghép từ Command và Popover | Trung bình |
| DatePicker | Calendar dựng trên react-day-picker | Trung bình, chọn khoảng ngày và chọn giờ phải tự lắp |
| Form, Form.Item | Form dựng trên react-hook-form và zod | Khó, khác biệt về mô hình, xem mục 7 |
| Table | Table ghép với TanStack Table | Khó nhất, xem mục 8 |
| Upload | Không có sẵn | Phải tự viết hoặc dùng react-dropzone |
| Tree, TreeSelect | Không có sẵn | Phải tự viết hoặc dùng thư viện chuyên biệt |
| Transfer | Không có sẵn | Phải tự viết hoàn toàn |
| Cascader | Không có sẵn | Phải tự viết hoàn toàn |
| Steps | Không có sẵn | Tự dựng bằng flex và biến CSS, khoảng một buổi làm việc |
| Descriptions | Không có sẵn | Dựng bằng lưới, rất nhanh |
| Spin | Skeleton hoặc tự dựng vòng xoay | Dễ |
| ConfigProvider theme token | Biến CSS trong file globals | Trung bình, xem mục 9 |
Nhìn bảng này sẽ thấy ngay điều quan trọng nhất: sáu component không có tương đương. Hãy đếm xem dự án bạn dùng bao nhiêu trong số đó và chúng xuất hiện ở bao nhiêu màn hình. Nếu con số lớn, chi phí thật của việc chuyển đổi cao hơn nhiều so với ước lượng ban đầu.
6. Xử lý giai đoạn hai thư viện cùng tồn tại
Trong nhiều tuần chuyển đổi, hai hệ thống kiểu dáng sẽ cùng chạy. Đây là ba xung đột thực tế và cách xử lý:
Xung đột thứ nhất: lớp reset của Tailwind đè lên giao diện cũ
Tailwind mang theo bộ chuẩn hóa kiểu dáng làm phẳng thẻ nút, tiêu đề và danh sách. Bật nó lên giữa một dự án đang dùng thư viện khác sẽ khiến nhiều chỗ trông sai lệch. Cách xử lý sạch nhất là dùng cơ chế lớp CSS để định thứ tự ưu tiên rõ ràng, đặt kiểu dáng của thư viện cũ vào một lớp đứng sau lớp chuẩn hóa của Tailwind.
Xung đột thứ hai: trùng tên lớp
Ant Design v5 cho phép đổi tiền tố tên lớp qua ConfigProvider. Đặt một tiền tố riêng cho thư viện cũ trong giai đoạn chuyển đổi giúp bạn tìm kiếm và xóa dấu vết dễ hơn về sau.
Xung đột thứ ba: hai hệ token màu
Đừng duy trì hai bảng màu song song. Hãy lấy biến CSS làm nguồn sự thật duy nhất ngay từ đầu, rồi cho ConfigProvider của thư viện cũ đọc giá trị từ chính các biến đó. Khi đó lúc gỡ thư viện cũ ra, màu sắc không đổi một chút nào.
7. Chuyển biểu mẫu: khác biệt về mô hình, không chỉ về cú pháp
Đây là phần khiến nhiều đội chuyển đổi thất bại vì đánh giá sai độ khó. Ant Design quản lý biểu mẫu bằng một thể hiện form nội bộ và các quy tắc kiểm tra khai báo trong props. React Hook Form đi theo hướng khác: mọi thứ dựa trên đăng ký trường và một lược đồ dữ liệu độc lập.
Cách viết cũ:
<Form form={form} onFinish={onSubmit}>
<Form.Item
name="email"
label="Email"
rules={[
{ required: true, message: 'Vui lòng nhập email' },
{ type: 'email', message: 'Email không hợp lệ' },
]}
>
<Input />
</Form.Item>
</Form>
Cách viết mới, lược đồ dữ liệu tách hẳn khỏi giao diện:
const schema = z.object({
email: z.string().min(1, 'Vui lòng nhập email').email('Email không hợp lệ'),
});
const form = useForm({ resolver: zodResolver(schema) });
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
Lợi ích thật sự không nằm ở số dòng mã mà ở chỗ lược đồ dữ liệu dùng lại được ở phía máy chủ để kiểm tra dữ liệu đầu vào, nên quy tắc kiểm tra ở hai đầu không bao giờ lệch nhau. Chi tiết kỹ thuật đầy đủ nằm ở bài React Hook Form kết hợp Zod.
| Khái niệm cũ | Tương đương mới |
|---|---|
| rules trong Form.Item | Lược đồ zod khai báo tập trung |
| form.setFieldsValue | form.setValue hoặc form.reset |
| form.getFieldsValue | form.getValues |
| form.validateFields | form.trigger |
| onFinish | form.handleSubmit |
| initialValues | defaultValues trong useForm |
| Form.List | useFieldArray |
| Kiểm tra liên trường bằng validator tùy biến | Phương thức refine hoặc superRefine của zod |
8. Chuyển bảng dữ liệu: phần tốn công nhất
Bảng của Ant Design là một component trọn gói: bạn khai báo cột và dữ liệu, nó lo hết sắp xếp, lọc, phân trang, chọn dòng, cố định cột. Bên phía mới, phần logic do TanStack Table đảm nhiệm còn phần hiển thị do bạn tự dựng. Đổi lại, bạn kiểm soát hoàn toàn cách bảng hiển thị trên màn hình điện thoại.
const columns = [
{ accessorKey: 'name', header: 'Tên sản phẩm' },
{
accessorKey: 'price',
header: 'Giá',
cell: ({ row }) => formatCurrency(row.getValue('price')),
},
];
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
Ước lượng công sức thực tế cho một bảng nghiệp vụ đầy đủ chức năng là từ một tới hai ngày công cho bảng đầu tiên, sau đó các bảng tiếp theo chỉ còn vài giờ vì bạn đã có sẵn component bảng dùng chung. Vì vậy hãy làm bảng phức tạp nhất trước, không làm bảng dễ nhất trước.
Một lưu ý về hiệu năng: nếu dữ liệu lấy từ máy chủ, hãy chuyển sắp xếp, lọc và phân trang về phía máy chủ ngay trong đợt này, đừng bê nguyên mô hình tải hết dữ liệu rồi xử lý ở trình duyệt. Kết hợp với TanStack Query để quản lý bộ nhớ đệm.
9. Chuyển hệ token màu và chế độ tối
Ant Design cấu hình chủ đề bằng một object token truyền vào ConfigProvider. Shadcn UI dùng biến CSS. Cách chuyển an toàn là dựng biến CSS trước, rồi cho ConfigProvider đọc lại từ biến đó trong giai đoạn giao thoa:
:root {
--brand: oklch(0.62 0.19 258);
--brand-foreground: oklch(0.98 0 0);
--radius: 0.5rem;
}
Nhờ vậy, đến ngày gỡ thư viện cũ, không có một mã màu nào bị bỏ sót vì chúng chưa bao giờ tồn tại ở hai nơi. Phần chế độ tối làm theo bài thiết kế Dark Mode chuẩn UX với Tailwind CSS.
10. Đừng đánh mất trợ năng trong lúc chuyển
Rủi ro lớn nhất khi bỏ một thư viện trưởng thành là mất đi những thứ vô hình đã được xử lý sẵn. May mắn là Radix UI Primitives xử lý phần này rất tốt, nhưng chỉ khi bạn dùng đúng cấu trúc component mà nó quy định. Hai lỗi hay gặp:
- Tự viết lại phần kích hoạt của hộp thoại bằng một thẻ div gắn sự kiện nhấp chuột thay vì dùng component kích hoạt có sẵn, làm mất hoàn toàn khả năng thao tác bằng bàn phím.
- Bỏ nhãn của trường nhập liệu và chỉ để văn bản gợi ý bên trong ô, khiến trình đọc màn hình không đọc được tên trường.
Hãy thêm một bước vào danh sách kiểm tra khi duyệt mã nguồn: mọi màn hình đã chuyển phải thao tác được trọn vẹn chỉ bằng bàn phím. Tham chiếu tiêu chuẩn ở bài Web Accessibility và bộ tiêu chuẩn WCAG 2.1.
11. Bảng ước lượng công sức theo loại màn hình
| Loại màn hình | Công sức ước lượng | Rủi ro chính |
|---|---|---|
| Trang tĩnh, trang giới thiệu | Dưới nửa ngày | Gần như không có |
| Danh sách đơn giản kèm bộ lọc | Nửa ngày tới một ngày | Lệch cách hiển thị trạng thái rỗng và trạng thái đang tải |
| Biểu mẫu dưới mười trường | Nửa ngày | Sai thông điệp lỗi so với bản cũ |
| Biểu mẫu nhiều bước, có mảng động | Hai tới ba ngày | Logic hiển thị trường phụ thuộc điều kiện |
| Bảng nghiệp vụ đầy đủ chức năng, bảng đầu tiên | Một tới hai ngày | Phân trang phía máy chủ, chọn nhiều dòng |
| Màn hình dùng Tree, Transfer, Cascader | Ba ngày trở lên mỗi component | Phải viết mới hoàn toàn, dễ vỡ phạm vi công việc |
| Gỡ thư viện cũ và dọn dẹp cuối cùng | Một ngày | Còn sót import ẩn trong nhánh mã ít chạy |
12. Danh sách kiểm tra trước khi gỡ thư viện cũ
- Tìm toàn bộ mã nguồn, không còn bất kỳ dòng import nào từ thư viện cũ, kể cả trong file kiểm thử và file cấu hình.
- Đã gỡ ConfigProvider và các thiết lập ngôn ngữ vùng miền đi kèm.
- Đã gỡ các gói phụ thuộc theo kèm mà không còn ai dùng, ví dụ thư viện xử lý ngày tháng của bản cũ.
- Chạy lại công cụ phân tích gói, đối chiếu với ba con số đã ghi ở mục 2 và lưu lại kết quả.
- Đo lại Core Web Vitals trên môi trường thật, không đo trên máy phát triển.
- Rà lại toàn bộ thông điệp lỗi của biểu mẫu, đây là chỗ hay bị dịch sót nhất.
- Kiểm tra giao diện trên màn hình điện thoại cho các bảng dữ liệu đã chuyển.
- Kiểm tra thao tác bàn phím và trình đọc màn hình ở các luồng quan trọng.
- Kiểm tra chế độ tối trên mọi màn hình, đây là chỗ hay sót màu viết cứng nhất.
13. Khi nào KHÔNG nên migrate
- Sản phẩm chỉ còn duy trì, không phát triển thêm. Bỏ công sức lớn cho phần mềm sắp dừng là lãng phí rõ ràng.
- Dự án nặng về component nghiệp vụ phức tạp. Nếu dùng nhiều Tree, Transfer, Cascader và bảng có ô gộp, bạn đang tự nhận việc viết lại một thư viện.
- Không có ai trong đội thạo Tailwind CSS. Chuyển xong sẽ có một hệ giao diện mà cả đội đều ngại sửa, tệ hơn hiện trạng.
- Đội không có quyền quyết định thời gian. Nếu không giành được cam kết dành thời gian cho việc này trong nhiều đợt phát hành liên tiếp, khả năng cao dự án sẽ đứng lại ở trạng thái nửa vời mang cả hai thư viện.
- Lý do duy nhất là dung lượng gói và bạn chưa thử các cách rẻ hơn ở mục 3.
Trạng thái nửa vời là kết cục tệ nhất: dung lượng tăng vì mang hai thư viện, giao diện không nhất quán giữa các màn hình, và người mới vào dự án không biết nên viết theo lối nào. Nếu không chắc đi được tới cuối, đừng bắt đầu.
Kết luận
Chuyển từ Ant Design sang Shadcn UI là bài toán quản trị dự án nhiều hơn là bài toán kỹ thuật. Ba nguyên tắc quyết định thành bại: đo con số thật trước khi quyết định, siết dần theo từng lớp thay vì viết lại một lần, và đặt hạn chót rõ ràng cho giai đoạn hai thư viện cùng tồn tại.
Uwmee Software nhận đánh giá hiện trạng và thực hiện chuyển đổi thư viện giao diện cho dự án React và Next.js đang chạy. Đọc thêm bài Shadcn UI là gì và bài tối ưu tốc độ website đạt Core Web Vitals.