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

2026-08-11 17 phút đọc 1 lượt xemBởi Uwmee Software

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ốngNên chuyểnNên giữ Ant Design
Loại sản phẩmTrang bán hàng, landing page, phần mềm dịch vụ hướng người dùng cuốiTrang 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ăngCó mục tiêu Core Web Vitals rõ ràng, có SEO, người dùng ở mạng chậmNgườ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ặngChủ yếu form, danh sách, thẻ nội dung, hộp thoạiNhiề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 tokenChưa từng dùng, quen viết CSS module hoặc styled-components
Quỹ thời gianCó thể chia nhỏ ra nhiều đợt trong 2 tới 4 thángChỉ 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ì:

  1. 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 đó.
  2. 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.
  3. 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 ánViệc phải làmHiệu quả thường thấy
Nạp động component nặngDù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ảoGiả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ừaRà 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 PurgingRú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ệtCả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:

  1. Đó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.
  2. 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.
  3. 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.
  4. 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ô.
  5. 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.
  6. 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 DesignTương đươngMức độ khó
ButtonButtonDễ, chỉ đổi tên thuộc tính type thành variant
Input, Input.TextAreaInput, TextareaDễ
ModalDialogDễ 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
DrawerSheetDễ
PopconfirmAlertDialogDễ
message, notificationsonnerDễ, cả hai đều gọi bằng hàm
SelectSelectTrung bình, tính năng tìm kiếm trong danh sách phải tự ghép bằng Combobox
AutoCompleteCombobox ghép từ Command và PopoverTrung bình
DatePickerCalendar dựng trên react-day-pickerTrung bình, chọn khoảng ngày và chọn giờ phải tự lắp
Form, Form.ItemForm dựng trên react-hook-form và zodKhó, khác biệt về mô hình, xem mục 7
TableTable ghép với TanStack TableKhó nhất, xem mục 8
UploadKhông có sẵnPhải tự viết hoặc dùng react-dropzone
Tree, TreeSelectKhông có sẵnPhải tự viết hoặc dùng thư viện chuyên biệt
TransferKhông có sẵnPhải tự viết hoàn toàn
CascaderKhông có sẵnPhải tự viết hoàn toàn
StepsKhông có sẵnTự dựng bằng flex và biến CSS, khoảng một buổi làm việc
DescriptionsKhông có sẵnDựng bằng lưới, rất nhanh
SpinSkeleton hoặc tự dựng vòng xoayDễ
ConfigProvider theme tokenBiến CSS trong file globalsTrung 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.ItemLược đồ zod khai báo tập trung
form.setFieldsValueform.setValue hoặc form.reset
form.getFieldsValueform.getValues
form.validateFieldsform.trigger
onFinishform.handleSubmit
initialValuesdefaultValues trong useForm
Form.ListuseFieldArray
Kiểm tra liên trường bằng validator tùy biếnPhươ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ìnhCông sức ước lượngRủi ro chính
Trang tĩnh, trang giới thiệuDưới nửa ngàyGần như không có
Danh sách đơn giản kèm bộ lọcNửa ngày tới một ngàyLệ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ườngNửa ngàySai thông điệp lỗi so với bản cũ
Biểu mẫu nhiều bước, có mảng độngHai tới ba ngàyLogic 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ênMột tới hai ngàyPhân trang phía máy chủ, chọn nhiều dòng
Màn hình dùng Tree, Transfer, CascaderBa ngày trở lên mỗi componentPhả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ùngMột ngàyCò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ũ

  1. 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.
  2. Đã gỡ ConfigProvider và các thiết lập ngôn ngữ vùng miền đi kèm.
  3. Đã 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ũ.
  4. 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ả.
  5. Đ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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.

Câu hỏi thường gặp

Bạn cần giải pháp phần mềm chuyên nghiệp?

Uwmee Software cung cấp dịch vụ 12 tester Google Play & thiết kế website cao cấp giúp nâng tầm thương hiệu của bạn.