Chuyện Xây Dựng Bộ Biên Dịch Phản Ứng Cho JavaScript Và Những Khó Khăn Gặp Phải
Tác giả chia sẻ trải nghiệm thiết kế bộ biên dịch hỗ trợ mô hình reactivity cho JavaScript với nhiều vấn đề kỹ thuật phức tạp như phân tích đa mô-đun, tương tác với Vite, và các thử thách trong khai triển hiệu năng thực tế. Cuối cùng, tác giả nhận ra khoảng cách lớn giữa ý tưởng và thực thi, dẫn đến quyết định dừng dự án.

Chuyện Xây Dựng Bộ Biên Dịch Phản Ứng Cho JavaScript Và Những Khó Khăn Gặp Phải
Tác giả chia sẻ về hành trình thiết kế một bộ biên dịch reactive dành cho JavaScript, nơi bạn chỉ cần viết let count = signal(0) hay const doubled = count * 2, còn việc cập nhật phức tạp sẽ do bộ biên dịch đảm nhiệm tự động. Ý tưởng là tạo ra framework với lượng code lặp lại tối thiểu, mang lại hiệu năng như Solid.js, nhưng cú pháp thân thiện giống React. Tuy nhiên, ngay từ đầu, tác giả đã đối mặt với nhiều vấn đề kỹ thuật khó nhằn có thể làm… phát điên.
Thách thức "Transfer vs. Expression"
Một biến count duy nhất nhưng lại phải xử lý rất khác nhau tùy ngữ cảnh sử dụng:
- Trả về (return): truyền đối tượng signal để bên gọi có thể đăng ký theo dõi thay đổi
- Gọi log: truyền giá trị hiện tại vì
console.logkhông hiểu signal - Truyền props trong JSX: vẫn truyền đối tượng signal để giúp re-render component con
- Toán học: truyền giá trị số thực để thực hiện phép tính
Điều này tạo ra vấn đề lớn cho compiler khi phải phân biệt được vị trí truyền biến (nơi compiler kiểm soát cả 2 bên) với vị trí biểu thức (bên ngoài kiểm soát, ví dụ hàm thư viện, toán tử JS).
Quy tắc tác giả đề ra:
Chỉ “transfer” signal object ở nơi compiler kiểm soát được cả sender và receiver.
Bất kỳ khi nào dữ liệu đi ra ngoài phạm vi kiểm soát compiler, phải bóc tách thành giá trị thuần để tránh lỗi.
Hậu quả khó chịu khi refactor
Ví dụ, phép nhân trong biểu thức reactive vẫn đúng khi viết trực tiếp, nhưng nếu tách ra hàm double(n) thì tính reactive biến mất vì compiler không kiểm soát được hàm đó. Điều này làm các thao tác tách hàm, refactor thông thường trở nên nguy hiểm, thay đổi hành vi một cách âm thầm.
Trong khi đó, các framework như Svelte 5 giải quyết bằng cách yêu cầu đánh dấu rõ ràng nơi tính toán reactive với $derived().
Phân tích đa mô-đun phức tạp
Compiler cần nhận biết biến nào là signal không chỉ trong file hiện tại mà là toàn bộ project. Để làm điều đó, tác giả thiết kế hệ thống hai bước:
- Quét tất cả các file để thu thập metadata (exports, hàm trả signal, imports)
- Dùng metadata này để transform từng file
Nhưng thực tế với các file barrel, xuất tái xuất, import động thì rất khó khăn. Hơn nữa, Vite – công cụ build phổ biến – lại gọi transform riêng lẻ từng file, không phù hợp với yêu cầu nhìn toàn bộ project trước transform.
Tác giả đứng trước lựa chọn khó: từ bỏ Vite, tự xây dựng pipeline mới, hoặc chấp nhận hệ thống cache phức tạp dễ lỗi.
Chiêu TypeScript "ảo" và sự đánh đổi
Để giữ trải nghiệm làm việc tốt, tác giả sử dụng thủ thuật Intersection Type cho hàm signal() giúp IDE tự động hoàn thành và không cần plugin riêng. Nhưng trong thực tế, khi signal truyền qua các hàm generic không được compiler transform, TypeScript “nói dối” về loại dữ liệu, gây lỗi runtime rất khó phát hiện.
Bài học rút ra là nếu type mô tả thứ không có thật khi chạy, sẽ có người gọi phương thức không tồn tại gây lỗi khó chịu.
Hydration và DOM: Tưởng dễ mà khó
Tác giả thiết kế compiler tạo các đường dẫn trực tiếp đến node DOM cần update khi hydration mà không dùng marker hoặc attribute phức tạp. Ý tưởng rất đẹp và nhanh.
Nhưng thực tế các extension như Grammarly hay trình quản lý mật khẩu tự động chèn node vào DOM làm phá vỡ chỉ số phần tử. Thêm nữa, trình duyệt tự thêm các thẻ HTML chuẩn như <tbody> làm sai lệch cây DOM so với source.
Cuối cùng tác giả phải thêm các comment marker để đảm bảo độ chính xác, làm mất đi sự “sạch” ban đầu.
Phần bẫy của destructuring
Tác giả rất tâm đắc khi làm cho phép “destructuring” reactive hoạt động mượt mà với các trường hợp phức tạp.
Tuy nhiên, spread props {...props} trong React là vấn đề lớn: Khi spread thì các getter reactive bị gọi hết tạo snapshot tĩnh, làm mất reactivity ngay lập tức.
Đây là pattern phổ biến nhất và không dễ giải quyết. Nhiều framework khác đã xây dựng các hàm utility để hỗ trợ (chẳng hạn splitProps, mergeProps ở Solid.js).
Mô hình reactive khó định nghĩa
Hệ thống batch updates được thiết kế với các đặc tính: push-based notification, lazy recomputation, tổ chức cập nhật DOM thông qua queueMicrotask cho hiệu năng và tránh glitch.
Song việc diễn đạt chính xác khái niệm "flush" hay "topological order" trong thực thi reactive lại gây mâu thuẫn. Đây là mô hình lai giữa push và pull, cần diễn giải chính thức bằng ngôn ngữ toán học mới chuẩn xác, điều không dễ dàng.
Kích thước runtime thực tế không như kỳ vọng
Ban đầu tác giả nghĩ runtime của framework chỉ tầm 3.2 KB, trong khi thực tế tính đầy đủ các phần như quản lý signal, theo dõi cleanup, mutations, reconciliation, giải mã dữ liệu rơi vào khoảng 8-10 KB cho một trang tương tác. Vẫn nhỏ hơn React nhưng không như bản thiết kế ban đầu.
Đây là hiện tượng phổ biến với nhiều framework: dự đoán ban đầu đi kèm kỳ vọng, khi thực hiện bị thực tế đè bẹp.
Đặc tả dài gần 55 KB, chưa có dòng code nào
Tác giả xây dựng đặc tả chi tiết, đầy đủ, bao quát mọi trường hợp khó về compiler, như closure, async, re-exports phức tạp, normalization HTML, các trường hợp corner case... tổng cộng 1500 dòng.
Nhưng không có một dòng code thực thi nào vì trật tự ưu tiên công việc và độ khó quá lớn cho một cá nhân.
Nỗi lòng của người viết compiler
Có lúc tác giả nhận ra sự thật nghiệt ngã: dù ý tưởng tốt, đặc tả đủ, nhưng để build được hệ thống hoàn chỉnh đòi hỏi cả đội ngũ chuyên sâu hàng năm, và nhiều thứ khó giải quyết (phân tích module, build tool không hợp tác, runtime lớn hơn...)
Cuối cùng tác giả phải chấp nhận rằng khoảng cách từ ý tưởng đẹp đến dự án thực dụng là rất xa, và bản thân cũng có công việc chính không thể bỏ.
Kết luận và bài học
Cuối cùng tác giả công bố sẽ dừng dự án framework, không phải vì thiếu đam mê hay ý tưởng kém, mà bởi hiểu rõ sự khác biệt lớn giữa việc hiểu vấn đề và giải quyết nó một cách hoàn chỉnh.
Dù không viết được code thực tế, việc làm đặc tả chi tiết đã giúp tác giả trưởng thành hơn, làm kỹ sư tốt hơn qua trải nghiệm quý giá này.
“Mã code tốt nhất bạn từng viết có thể là mã bạn quyết định không phát hành.”
Dự án vẫn được giữ trong repo, khi nào buồn người ta lại mở ra xem cho thỏa… Nhưng rồi quay lại với React - đó là cuộc sống.
Bài viết cho chúng ta cái nhìn sâu sắc về mặt kỹ thuật rất thực tế khi phát triển một framework reactive, đặc biệt với ngôn ngữ JavaScript vốn đa dạng và phức tạp.
Với các lập trình viên và nhà phát triển tại Việt Nam, đây cũng là bài học về giới hạn công cụ build hiện tại (như Vite), cũng như về cú pháp reactive và các trade-off cần cân nhắc khi thiết kế framework.
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
