Từ Ag-Grid đến shadcn: chín năm, ba câu hỏi cũ
Chín năm đi qua nhiều stack frontend — và ba câu hỏi vẫn sống sót qua tất cả.
Năm 2017 tôi đang gắn jQuery vào một project .NET ở FPT Software Sài Gòn. Grid là Ag-Grid, build bằng MSBuild, deploy là ai đó bê file qua server. Năm 2026 tôi đang ghép các primitive của shadcn lên Next.js Server Components cho một storefront tự vận hành. Phần lớn những gì tôi học được ở giữa hai mốc đó vẫn còn dùng được — chỉ là bị đổi tên.
Cái grid đầu tiên tôi ship
Feature đầu tiên tôi tự tay làm từ đầu đến cuối là một báo cáo năng suất tester ở FPT. Trên giấy tờ tôi là manual tester, nhưng team thiếu một frontend developer và báo cáo cần filter, sort, group cột theo tuần. Tôi nhét Ag-Grid vào trang, copy config từ một đồng nghiệp, rồi ship.
Nó chậm. Sort là tab bị đơ vài giây với vài ngàn dòng — vì tôi đẩy hết dữ liệu xuống client và để Ag-Grid sort trong memory. Anh lead không bảo tôi chuyển sang server-side row model. Anh ấy hỏi: "user muốn nhìn thấy cái gì đầu tiên?" Câu trả lời là: các case fail của tuần trước, gom theo feature. Số dòng hiển thị tụt xuống hơn một bậc. Trang nhanh hẳn mà tôi không động đến Ag-Grid.
Bài học frontend đầu tiên tôi còn giữ là — performance thường là câu hỏi product giả dạng câu hỏi kỹ thuật.
Năm 2017 tôi chưa có ngôn từ cho mấy thứ này. Tôi tự viết một cái bug ticket dài cho chính mình, sửa query, rồi đi tiếp. Grid vẫn ở đó. Chắc giờ nó vẫn ở đó.
Những năm React, khi state nuốt mất cái calendar
Năm 2020 tôi sang Remolution và chơi all-in với React. Sản phẩm là một recruiting platform với tín hiệu real-time — trạng thái interviewer, hold lịch, payment event đổ về từ webhook của provider. Stack frontend lúc đầu là CRA, Redux, redux-saga, react-router. Đến cuối thì là Next.js Pages Router, Zustand cho client state, SWR cho server state, và một component calendar tôi đã viết lại ba lần.
Lần viết lại thứ ba là lần tôi nhớ. Có một bug lặp lại — recruiter kéo một interview sang slot mới, optimistic update đậu xuống, rồi ba mươi giây sau nó âm thầm rollback. Interviewer ở đầu kia có mặt đúng giờ cũ. Tôi ship bản viết lại thứ ba cuối 2022. Fix không nằm ở calendar — nó nằm ở cách tôi reconcile event websocket với cache local. Tôi đang coi websocket là source of truth, còn optimistic update là một lời nói dối tạm thời — nhưng lời nói dối mới là câu trả lời đúng hơn trong phần lớn trường hợp.
Tôi viết lại reconciliation để giữ optimistic state cho đến khi server xác nhận hoặc gửi về một conflict có cấu trúc. Calendar hết rollback. Đừng mô hình server state như sự thật cuối cùng khi user vừa nói cho bạn sự thật cách đây mười mili-giây. Nó vẫn áp dụng gần như nguyên văn cho các hệ agent tôi đang xây bây giờ — chỉ khác là "user" đôi khi là một agent khác.
Fullstack kéo tiếng ồn framework xuống một tầng
Năm 2023 tôi chuyển sang phía fullstack của cùng sản phẩm. Lúc đó đã có hơn một trăm enterprise customer. Các cuộc trao đổi về frontend không còn là về library nào nữa, mà là về seam nào.
Server-render pipeline view của recruiter? Bottleneck không phải React — là query Postgres chạy RLS qua ba join. Một ô search cảm giác ì? Không phải vấn đề debounce — thiếu index GIN. Code frontend thật sự co lại. Tôi xoá nguyên reducer khi đẩy state của nó ra sau một server action. Việc migrate sang shadcn bắt đầu ở đây, gần như tình cờ. Bọn tôi cần một Combobox không đánh nhau với focus management của Radix, và recipe của shadcn là đường ngắn nhất. Vài tháng sau bọn tôi đã thay nửa design system mà phía customer không nhận ra.
Điều tôi sẽ nói với tôi của ngày trước
Cuộc tranh luận framework càng yên đi khi bạn ngồi càng gần data. Nếu một cái button mãi không thấy "đúng", câu trả lời gần như không bao giờ nằm trong cái button.
Tôi ship một feature trong giai đoạn này mà đến giờ vẫn còn tự hào — một candidate timeline kéo dữ liệu từ bốn event source khác nhau, render server-side với streaming. Bản đầu có một nhịp flash trống vì tôi await cả bốn source song song trước khi gửi byte đầu tiên. Fix là cho mỗi source chạy như một Suspense boundary riêng. Flash biến mất. Wall-clock tổng vẫn vậy. User nói cảm giác nhanh hơn nhiều.
Những gì shadcn làm lộ ra
Ở PSA tôi đang xây agentic commerce — autonomous storefront, nơi một LLM chọn sản phẩm, viết copy, và quyết định khi nào giảm giá. Bề mặt UI nhỏ, nhưng mỗi component phải có thể được agent theme lại lúc runtime. shadcn hợp với việc này vì nó không phải library — nó là một folder component tôi sở hữu, viết bằng primitive tôi có thể re-style mà không cần fork. Khi agent muốn một CTA mềm hơn cho người mua đang phân vân, tôi đưa nó một token để lật, chứ không phải một component để swap.
Đây là stack đầu tiên tôi làm mà frontend không phải phần chậm nhất của sản phẩm. Inference latency mới là. Một cái button render tức thì chẳng có nghĩa gì khi model đằng sau mất gần hai giây để quyết button đó nên nói cái gì. Nên công việc frontend chuyển sang giấu latency — streaming output từng phần, placeholder optimistic, loading state có cấu trúc, không nói dối về cái đang tới.
Nếu frontend của 2020 là "render sự thật cho nhanh", frontend của 2026 là "render một phỏng đoán hữu ích trong lúc sự thật đang đến."
Ba câu hỏi đã sống sót
Chín năm — tên framework đổi năm lần, còn câu trả lời cho "user nên thấy gì đầu tiên?" thì không đổi lần nào. Ba câu hỏi vẫn gánh mọi quyết định frontend tôi đưa ra.
Cái user đang chờ có thật sự là cái đang ngốn thời gian không? Đa phần là không. Bottleneck di chuyển từ sort trong browser sang network round-trip rồi sang inference của model — nhưng hình dạng câu hỏi giống hệt nhau.
Interface có nói thật về việc đang xảy ra không, kể cả khi chưa có gì xảy ra? Empty state, loading state, conflict state — cùng một bài toán mặc ba bộ đồ khác nhau.
Khi hệ thống bất đồng với user, ai thắng, và nhanh tới mức nào? Optimistic update, undo, override path — đều là biến thể của câu hỏi này.
Ag-Grid, Redux, SWR, shadcn, Server Components, agent runtime. Công cụ thì đổi. Câu hỏi thì không.