Từ Turbo Streams sang Turbo Morph: Đơn giản hóa Real-Time trong Rails
Bài viết chia sẻ hành trình chuyển đổi từ Turbo Streams sang Turbo Morph trong ứng dụng Rails, giúp cắt giảm hàng trăm dòng code và giải quyết vấn đề phức tạp khi đồng bộ dữ liệu real-time. Phương pháp mới này giúp giảm thiểu lỗi dữ liệu cũ và đơn giản hóa kiến trúc tổng thể của hệ thống.

Trong quá trình xây dựng hệ thống quản lý đơn hàng đa tenant cho các quán cà phê và nhà hàng, tôi đã gặp phải một bài toán thú vị về kiến trúc. Hệ thống yêu cầu cập nhật thời gian thực (real-time) trên nhiều màn hình khác nhau: bếp nhìn thấy danh sách đơn hàng, nhân viên phục vụ theo dõi trạng thái món ăn, và tất cả đều phải đồng bộ hóa tức thì.
Ban đầu, lựa chọn tự nhiên trong Rails là Turbo Streams — công cụ cho phép cập nhật DOM mục tiêu qua WebSocket. Thay thế một phần, thêm vào danh sách, hoặc xóa bỏ phần tử. Cách này hoạt động tốt trong giai đoạn đầu, nhưng khi hệ thống mở rộng, những vấn đề nghiêm trọng đã bắt đầu nảy sinh.
Chi phí của phương pháp Targeted Broadcasts
Khi tôi bắt đầu xây dựng tính năng "bếp thực đơn" (kitchen queue), nơi đầu bếp xem các đơn hàng đang chờ, nhu cầu đồng bộ trở nên phức tạp hơn bao giờ hết. Mỗi thay đổi trạng thái của một món ăn phải cập nhật đồng thời trên bốn màn hình: trang đơn hàng, view bếp, view bàn và view mang đi.
Phương pháp phát sóng có mục tiêu (targeted broadcasts) nhanh chóng trở nên quá tải. Thay vì một tính năng mới chỉ thêm vài dòng code, nó lại nhân số lượng các phương thức phát sóng, ID đích của DOM, và các tệp partial lên gấp bội. Mô hình LineItem của tôi bị phình lên tới hàng trăm dòng code chỉ để xử lý việc phát sóng các trạng thái khác nhau (nấu, sẵn sàng, hủy, giao...).
Mỗi lần phát sóng yêu cầu phải khớp chính xác: tên kênh, ID mục tiêu DOM, đường dẫn partial và dữ liệu locals mới nhất. Một sai sót nhỏ cũng dẫn đến giao diện không cập nhật hoặc hiển thị sai thông tin.
Hai vấn đề chính: Dữ liệu cũ và Hiệu ứng nhấp nháy
Turbo Streams truyền thống gây ra hai loại lỗi nghiêm trọng mà Turbo Morph giải quyết hoàn toàn:
- Dữ liệu cũ (Stale Data): Khi một callback kích hoạt, mặc dù
self(đối tượng hiện tại) có thể đã được cập nhật, nhưng các liên kết (associations) vẫn được lưu trong bộ nhớ đệm. Điều này khiến giao diện hiển thị trạng thái lỗi thời vì nó lấy dữ liệu từ bộ nhớ cache thay vì database mới nhất. Giải pháp buộc phải gọi thủ công.reloadkhắp nơi, khiến code trở nên lộn xộn. - Phát sóng kép (Double Broadcasts): Khi nhiều mô hình kích hoạt phát sóng trong cùng một commit, cùng một mục tiêu DOM có thể bị thay thế hai lần liên tiếp, gây ra hiệu ứng nhấp nháy (flicker) khó chịu cho người dùng.
Chuyển đổi sang Turbo Morph
Turbo 8 đã giới thiệu tính năng "làm mới trang với morphing" thông qua turbo_refreshes_with method: :morph. Thay vì phẫu thuật từng phần tử DOM riêng lẻ, nó yêu cầu mọi trình duyệt đang kết nối: "hãy tải lại trang này và tôi sẽ so sánh sự khác biệt để cập nhật (morph)".
Kết quả là mã nguồn trong mô hình LineItem giảm từ hơn 100 dòng phức tạp xuống chỉ còn vài dòng:
after_update_commit :broadcast_refreshes, if: :saved_change_to_status?
private
def broadcast_refreshes
broadcast_refresh_to "order_#{order_id}"
broadcast_refresh_to "store_#{order.store_id}_kitchen"
broadcast_refresh_to "store_#{order.store_id}_tables"
broadcast_refresh_to "store_#{order.store_id}_takeouts"
end
Không còn partial, không còn ID mục tiêu, không còn locals và không còn dữ liệu cũ.
Sự thay đổi trong Views và Controllers
Trong Views, tôi chỉ cần thêm khai báo morph và đảm bảo thuộc tính scroll: :preserve được thiết lập để giữ nguyên vị trí cuộn trang khi cập nhật — điều cực kỳ quan trọng với các danh sách dài mà nhân viên đang theo dõi.
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
<%= turbo_stream_from "store_#{Current.store.id}_kitchen" %>
Về phía Controllers, mã nguồn trở nên cực kỳ gọn gàng. Thay vì phải xử lý cả phản hồi HTML và Turbo Stream với các template riêng biệt, controller giờ chỉ cần redirect đơn giản.
Mọi cập nhật real-time đều do callback trong mô hình xử lý tự động.
Tổng kết hiệu quả và Khi nào nên dùng
Việc chuyển đổi này đã giúp tôi xóa bỏ khoảng 268 dòng code, bao gồm các phương thức broadcast, template Turbo Stream và các Stimulus controller chỉ để xử lý việc cập nhật DOM. Server luôn hiển thị "thực tế" duy nhất của dữ liệu mà không lo sai lệch do bộ nhớ đệm.
Sử dụng Turbo Morph khi:
- Nhiều view cần đồng bộ với nhau.
- Dữ liệu có các quan hệ phụ thuộc phức tạp.
- Bạn muốn cập nhật real-time mà không muốn quản lý sự phức tạp của DOM.
- Các trang của bạn đủ nhẹ để có thể render lại nhanh.
Sử dụng Targeted Turbo Streams khi:
- Bạn cần phản ứng với các sự kiện cụ thể ở phía client (ví dụ: chạy âm thanh khi có đơn mới).
- Việc render lại toàn bộ trang thực sự quá tốn kém tài nguyên.
- Bạn chỉ có một mục tiêu duy nhất, rõ ràng thay đổi biệt lập.
Đôi khi, giải pháp tốt nhất là ngừng cố gắng kiểm soát mọi thứ chi tiết và để framework làm việc nặng nhọc đó thay cho bạn.



