15 điểm bởi GN⁺ 2025-06-02 | 1 bình luận | Chia sẻ qua WhatsApp
  • Giống như JPEG lũy tiến, dữ liệu JSON cũng có thể được gửi trước trong trạng thái chưa hoàn chỉnh để client dần dần tận dụng được toàn bộ nội dung
  • Cách phân tích cú pháp JSON hiện tại có vấn đề kém hiệu quả: không thể làm gì cho đến khi toàn bộ dữ liệu được nhận đầy đủ
  • Dữ liệu được chia thành nhiều chunk (phần) theo cách breadth-first, trong đó các phần chưa sẵn sàng được biểu diễn bằng Promise và được lấp dần khi đã sẵn sàng, cho phép client sử dụng cả dữ liệu chưa hoàn thiện
  • Khái niệm này là đổi mới cốt lõi của React Server Components (RSC), và kiểm soát trạng thái tải theo từng giai đoạn có chủ đích thông qua <Suspense>
  • Có thể tách biệt việc streaming dữ liệu và luồng tải UI có chủ đích để mang lại trải nghiệm người dùng linh hoạt hơn

Ý tưởng về JPEG lũy tiến và JSON lũy tiến

  • JPEG lũy tiến không tải ảnh theo kiểu từ trên xuống dưới trong một lần, mà trước tiên hiển thị toàn bộ ảnh ở trạng thái mờ rồi dần trở nên sắc nét hơn
  • Tương tự, áp dụng cách tiếp cận lũy tiến cho việc truyền JSON cho phép sử dụng ngay một phần dữ liệu mà không phải đợi toàn bộ hoàn tất
  • Với cấu trúc dữ liệu JSON ví dụ, cách thông thường chỉ có thể parse sau khi đã nhận đến byte cuối cùng
  • Vì vậy client phải chờ đến khi mọi thứ được truyền xong, kể cả phần chậm của server (ví dụ: tải comments từ DB chậm), và đây là tiêu chuẩn hiện tại rất kém hiệu quả

Giới hạn của trình phân tích cú pháp JSON streaming

  • Nếu dùng trình phân tích cú pháp JSON streaming thì có thể tạo ra cây đối tượng dữ liệu chưa hoàn chỉnh (trung gian)
  • Tuy nhiên, khi các trường của từng đối tượng (ví dụ: footer, danh sách nhiều comment, v.v.) chỉ được truyền một phần, sẽ phát sinh vấn đề không khớp kiểu và khó xác định đã hoàn tất hay chưa, làm giảm khả năng sử dụng
  • Tương tự như render HTML theo kiểu streaming, nếu xử lý stream theo thứ tự thì một phần chậm có thể làm chậm toàn bộ kết quả
  • Đây là lý do việc tận dụng JSON streaming nói chung không phổ biến

Đề xuất cấu trúc Progressive JSON

  • Thay vì streaming theo kiểu duyệt sâu của cách truyền cũ (tức truyền bằng cách đi sâu vào các nhánh con của cây), mô hình này áp dụng breadth-first
  • Chỉ gửi đối tượng cấp cao nhất trước, còn các giá trị con được đặt thành placeholder giống Promise, rồi được điền dần bằng các chunk riêng khi sẵn sàng
  • Ví dụ, mỗi khi server hoàn tất việc tải dữ liệu bất đồng bộ thì gửi chunk tương ứng, và client chỉ sử dụng những gì đã sẵn sàng
  • Nhờ đó có thể nhận dữ liệu bất đồng bộ (tải sớm) mà không phải chờ toàn bộ nhiều phần chậm xử lý xong
  • Nếu client được thiết kế vững vàng cho việc nhận chunk không tuần tự và bán tuần tự, server có thể linh hoạt áp dụng nhiều chiến lược chia chunk khác nhau

Inlining và Outlining: truyền dữ liệu hiệu quả

  • Định dạng streaming JSON lũy tiến có thể trích riêng các đối tượng được tái sử dụng (ví dụ: cùng một userInfo được tham chiếu ở nhiều nơi) thành một chunk duy nhất để các vị trí khác nhau cùng tham chiếu mà không cần lưu trùng lặp
  • Chỉ tách phần chậm ra rồi gửi dưới dạng placeholder, còn phần còn lại được điền ngay để tạo luồng dữ liệu hiệu quả
  • Khi cùng một đối tượng xuất hiện nhiều lần, có thể chỉ truyền một lần rồi tái sử dụng (Outlining)
  • Theo cách này, cả tham chiếu vòng (cấu trúc đối tượng tự tham chiếu chính nó) cũng có thể được serialize tự nhiên bằng cấu trúc tham chiếu gián tiếp giữa các chunk, thay vì gặp khó khăn như JSON thông thường

Triển khai streaming lũy tiến của React Server Components (RSC)

  • Trên thực tế, React Server Components là ví dụ tiêu biểu áp dụng mô hình streaming JSON lũy tiến
    • Server sử dụng cấu trúc tải dữ liệu bên ngoài (ví dụ: Post, Comments) theo kiểu bất đồng bộ
    • Ở phía client, các phần chưa đến được giữ dưới dạng Promise, và UI được render dần theo thứ tự dữ liệu sẵn sàng
  • Thiết lập trạng thái tải có chủ đích bằng <Suspense> của React
    • Để tránh những lần nhảy giao diện không cần thiết gây khó chịu cho trải nghiệm người dùng, trạng thái Promise (các “lỗ trống”) không được hiển thị ngay mà có thể dàn dựng tải theo từng bước bằng <Suspense> fallback
    • Ngay cả khi dữ liệu đến nhanh, nhà phát triển vẫn có thể kiểm soát để UI thực tế được lộ ra dần theo các giai đoạn đã thiết kế

Tóm tắt và hàm ý

  • Đổi mới cốt lõi của React Server Components nằm ở cách streaming dần các props của cây component từ ngoài vào trong
  • Vì vậy không cần chờ server chuẩn bị xong toàn bộ dữ liệu; có thể hiển thị phần quan trọng trước và đồng thời kiểm soát chi tiết trạng thái chờ tải
  • Không chỉ riêng bản thân streaming, mà còn cần hỗ trợ ở cấp mô hình lập trình mang tính cấu trúc như <Suspense> của React để tận dụng nó
  • Nhờ đó có thể giảm bớt các nút thắt cổ chai của cách truyền cũ, như việc một phần dữ liệu chậm làm trì hoãn toàn bộ

1 bình luận

 
GN⁺ 2025-06-02
Ý kiến trên Hacker News
  • Có vẻ một số người đang hiểu bài viết này quá theo nghĩa đen; ý ở đây là Dan Abramov không hề đề xuất một định dạng mới tên là Progressive JSON
    • Bài viết này giải thích ý tưởng của React Server Components và quá trình biểu diễn cây component dưới dạng đối tượng JavaScript rồi truyền nó dưới dạng stream
    • Cách này cho phép đặt các “lỗ” trong cây component React, để ban đầu hiển thị trạng thái loading hoặc skeleton UI, rồi khi nhận được dữ liệu thật từ server thì render đầy đủ phần đó
    • Nhờ vậy có thể hiển thị loading ở mức chi tiết hơn và đưa màn hình đầu tiên lên nhanh hơn
  • Theo tôi thì việc mọi người mở rộng ý tưởng này và áp dụng sang các cách khác cũng không sao
    • Ý định là mô tả cách serialize dữ liệu của RSC không chỉ gói trong React mà như một pattern tổng quát hơn
    • Mong rằng nhiều ý tưởng tìm thấy trong React Server Components sẽ được hấp thụ vào các công nghệ hay hệ sinh thái khác
  • Tôi không thực sự thích kiểu progressive loading này, đặc biệt là trải nghiệm nội dung cứ liên tục dịch chuyển (jump)
    • Mẫu hiển thị UI trống trong lúc loading là thứ khiến tôi khá khó chịu
  • Hồi còn dùng Ember cách đây không lâu cũng có kiểu tương tự, và tôi nhớ việc viết Ajax endpoint cực kỳ đau khổ
    • Có vẻ mục đích là sắp xếp lại cấu trúc cây để một số phần tử con nằm ở cuối file, từ đó xử lý DAG (đồ thị không chu trình) hiệu quả hơn
    • Nếu dùng streaming parser kiểu SAX thì có thể bắt đầu paint trước khi toàn bộ dữ liệu đến nơi
    • Nhưng trong VM đơn luồng, nếu thiết kế sai thứ tự công việc thì rủi ro là vấn đề còn lớn hơn
  • Tôi đã thực sự dùng kiểu streaming partial JSON (Progressive JSON) này khi tích hợp với các công cụ AI
    • Trải nghiệm thực tế của tôi là cách này không chỉ hữu ích cho RSC mà còn có giá trị cho cả client lẫn server trong nhiều ngữ cảnh khác nhau
  • Tôi đã theo dõi cả bài nói "2 computers" của Dan và các bài đăng gần đây về RSC
    • Dan là người giải thích giỏi nhất trong hệ sinh thái React, nhưng nếu một công nghệ phải được giải thích khó đến mức này thì
      1. hoặc là công nghệ đó thực sự không cần thiết
      2. hoặc là abstraction đang có vấn đề
    • Phần lớn lập trình viên frontend vẫn chưa thực sự hiểu trọn vẹn khái niệm RSC
    • Vercel đã biến Next.js thành framework React mặc định, và việc chấp nhận RSC cũng lan rộng theo làn sóng đó
    • Ngay cả người dùng Next.js cũng chưa hiểu rạch ròi ranh giới của Server Component, và có nhiều trường hợp áp dụng theo kiểu “cargo cult”
    • Việc React không nhận PR liên quan đến Vite cũng đáng nghi. Có cảm giác cú đẩy RSC trên thực tế không hẳn vì user hay developer, mà có thể là chiến lược bán nền tảng hosting của các công ty nền tảng
    • Nhìn lại thì việc Vercel tuyển dụng ồ ạt đội ngũ React gốc cũng trông giống như muốn dẫn dắt tương lai của React
    • Cũng có ý kiến phản bác rằng nhận định về động cơ và bối cảnh lịch sử là không đúng, đồng thời giải thích thêm về tình hình hỗ trợ Vite
    • Có nhắc rằng việc tích hợp Vite hiện đang được chính đội Vite cải thiện do ràng buộc kỹ thuật là môi trường DEV cần bundling
    • Lập luận rằng mọi người không hiểu RSC bị xem là một kiểu lý luận vòng tròn
    • Có thể bạn không thích RSC, nhưng trong đó vẫn có nhiều ý tưởng thú vị đáng để công nghệ khác mượn dùng
    • Thay vì cố thuyết phục, mong rằng mỗi người tự lấy phần nào thấy lạ và hữu ích
  • Tất nhiên vẫn có thể làm SPA thành site tĩnh rồi đẩy lên CDN, và Next.js cũng có thể self-host ở chế độ “dynamic”
    • Tuy vậy, hiện thực là rất khó triển khai đầy đủ toàn bộ tính năng serverless rendering của Next.js ở nơi khác ngoài Vercel (vì có nhiều “magic” không được document)
    • Về vấn đề này cũng đã có đề xuất chính thức đưa adapter vào để cung cấp API nhất quán trên nhiều nền tảng: https://github.com/vercel/next.js/discussions/77740
    • Quan điểm của tôi là cú đẩy RSC có lẽ không xuất phát từ lợi ích doanh nghiệp, mà từ việc nhận ra pattern xây dựng website truyền thống (SSR + một chút progressive enhancement ở client) thật ra có nhiều ưu điểm
    • Chỉ với SSR thôi cũng đã có vấn đề là business logic bị đẩy sang client quá nhiều một cách không cần thiết
  • Bản thân RSC là công nghệ thú vị, nhưng trong thực chiến thì tôi thấy không mấy hợp lý
    • Có gánh nặng phải duy trì backend server Node/Bun ở quy mô lớn để render các component phức tạp
    • Thà dùng trang tĩnh hoặc kết hợp React SPA + server API viết bằng Go còn hiệu quả hơn nhiều
    • Có thể làm ra kết quả tương tự với lượng tài nguyên nhỏ hơn đáng kể
  • Việc giải thích một công nghệ mới phức tạp không nhất thiết có nghĩa nó vô dụng hay abstraction sai; cũng có những bài toán xứng đáng để chấp nhận độ phức tạp đó
    • Cứ chờ xem công nghệ này sẽ tiến hóa thế nào trong tương lai
  • Có ý kiến cho rằng có thể dùng cấu trúc code của RSC để build trang tĩnh bằng cách chia HTML/CSS/JS thành các mảnh nhỏ
    • Nếu thay placeholder ‘$1’ được đề xuất trong bài bằng URI thì có khi còn không cần server (đa số trường hợp không nhất thiết phải có SSR động)
    • Điểm yếu là với cách này, khi nội dung thay đổi thì tốc độ của pipeline cập nhật sẽ rất quan trọng, đặc biệt là streaming deployment của static site đã compile lên S3
    • Ví dụ như trang báo có nhiều bài được prerender, khi chỉ một phần nội dung thay đổi thì cần xử lý diff nội dung thông minh để chỉ build lại phần đó một cách hiệu quả
  • Trong thực tế công việc, người ta nói đến tối ưu hiệu năng nhưng lại tải nhiều MB dữ liệu và xử lý logic phức tạp ở frontend để tiết kiệm vài mili giây; trong khi giải pháp năng suất hơn nhiều thường là BFF, cải thiện kiến trúc hoặc xây API gọn hơn
    • Đã từng có các nỗ lực như GraphQL, http2, nhưng cuối cùng đó không phải lời giải cho vấn đề cốt lõi; nếu không có sự tiến hóa của web standard thì sẽ khó có thay đổi về mặt paradigm
    • Framework mới cũng vướng cùng giới hạn này
  • RSC, như được giải thích ở cuối bài viết, về bản chất là một BFF (Backend for Frontend)
  • Có ý kiến cho rằng còn tùy ta muốn rút ngắn “ms của page loading” theo nghĩa nào
    • Nếu tối ưu Time to first render và time to visually complete, thì cách gửi skeleton UI rỗng trước rồi nhận dữ liệu qua API để hydrate cho cảm giác nhanh nhất
    • Ngược lại, nếu muốn đẩy nhanh time to first input và time to interactive, thì cần render ngay dữ liệu người dùng, và trong trường hợp này backend có lợi thế hơn nhiều vì giảm số lần gọi mạng
    • Trong đa số trường hợp người dùng thích hướng này hơn; với ứng dụng CRUD SaaS thì server-side rendering phù hợp hơn, còn ứng dụng coi trọng thiết kế như Figma thì dữ liệu tĩnh ở client + fetch thêm dữ liệu sẽ hợp hơn
    • Không có “một lời giải cho mọi bài toán”, và điểm tối ưu là lựa chọn mang tính chủ quan
    • Trải nghiệm phát triển, cấu trúc đội ngũ và nhiều yếu tố khác cũng ảnh hưởng đến quyết định công nghệ
  • Nhờ đó mà tôi hiểu vì sao khi Facebook load thì nội dung cốt lõi luôn được render sau cùng
  • Có người hỏi BFF ở đây là gì
  • Có phản ứng rằng có quá nhiều chữ viết tắt nên không rõ FE và BFF là gì
  • Tôi không muốn tự mình dùng ý tưởng Progressive JSON này, và tôi nghĩ có nhiều phương án thay thế
    • Cách đơn giản nhất là chia một đối tượng JSON khổng lồ thành nhiều phần, tức truyền dưới dạng ‘JSON lines’
    • Thông tin header gửi một lần, còn mảng lớn thì gửi từng dòng để tối ưu xử lý stream
    • Nếu đối tượng còn lớn hơn thì có thể áp dụng cách này đệ quy, dù như vậy sẽ nhanh chóng trở nên quá phức tạp
    • Cũng có thể để server bảo đảm rõ ràng thứ tự các thuộc tính để progressive parsing và tách xử lý
    • Rốt cuộc cách này có lẽ không hữu ích lắm với cấu trúc thực sự khổng lồ, nhưng với những tình huống phổ biến khi xử lý JSON lớn thì lại khá thực dụng
  • Không nhất thiết phải đánh dấu “lỗ” một cách tường minh; cũng có thể gửi tuần tự các thông điệp stream và chỉ gửi delta (diff)
    • Dùng định dạng delta tên là ‘Mendoza’ thì có thể truyền patch (diffs) rất gọn trong Go và JS/Typescript: https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
    • Giống như cách diff nhị phân của zstd hay Mendoza, có thể chỉ giữ một phần dữ liệu đã serialize trong bộ nhớ để vá một cách hiệu quả
    • React cũng cần cách so sánh khác biệt hay chèn chỉ những thay đổi, nên đây là hướng tiếp cận có ý nghĩa
  • Trong streaming dữ liệu UI, chỉ mảng rỗng hoặc null là chưa đủ; cần có thông tin riêng để biết dữ liệu nào đang ở trạng thái pending, chưa tới nơi
    • Streaming payload của GraphQL chọn cách pha trộn giữa schema dữ liệu hợp lệ, thông tin phần chưa đến, và cơ chế patch về sau
  • Cần biết phần nào là “lỗ” thì mới dễ hiển thị trạng thái loading
  • Nếu muốn progressively decode JSON ở client, có thể tham khảo thư viện jsonriver: https://github.com/rictic/jsonriver
    • API rất đơn giản, hiệu năng tốt và đã được test đầy đủ
    • Nó parse các mảnh chuỗi được stream tới thành những giá trị ngày càng hoàn chỉnh hơn
    • Bảo đảm kết quả cuối cùng giống hệt JSON.parse
  • Có ý kiến cho rằng nếu là dữ liệu dạng cây thì đây là cách khá thú vị và áp dụng được cho nhiều loại cấu trúc
    • Dữ liệu cây có thể biểu diễn bằng các vector parent, type, data và string table, khi đó phần còn lại đều có thể rút xuống thành một số ít số nguyên
    • Gửi trước string table và type info như phần header, rồi stream các chunk vector parent, data theo đơn vị node
    • Streaming theo depth-first hay breadth-first chỉ cần đổi thứ tự các chunk là đủ
    • Có vẻ sẽ cải thiện khá nhiều UX thời gian tải trong các ứng dụng chạy qua mạng
    • Có thể trực quan hóa cây trên web theo bất kỳ thứ tự nào bằng cách gửi xen kẽ table và node chunk
    • Chỉ cần preorder traversal và thông tin depth là có thể khôi phục cấu trúc cây ngay cả khi không có node id
    • Làm một thư viện nhỏ từ ý tưởng này cũng là thử nghiệm đáng giá
  • Có lập luận rằng đa số ứng dụng không cần hệ thống loading “tinh vi” như thế này, và phần lớn trường hợp chỉ cần gọi API vài lần là xong
    • Phản hồi là mục đích chỉ là giải thích cách wire protocol của RSC hoạt động, chứ không phải khuyến khích ai đó tự đi triển khai thứ này
    • Hiểu nguyên lý giữa các công cụ khác nhau cuối cùng vẫn giúp ích khi muốn mượn hoặc remix ý tưởng ở nhiều nơi
    • Có người cho rằng chiến lược gọi nhiều lần sẽ tạo ra vấn đề n*n+1, nhưng thay vì truyền lồng đối tượng theo kiểu OOP/ORM thì cũng có thể truyền phẳng như ví dụ comment
    • Trong trường hợp đó, endpoint kiểu định nghĩa rõ type như protobuf cũng có điểm mạnh riêng
    • Nếu tách comments ra thì chỉ cần 2 lần gọi là đủ (trang+bài viết, và phần bình luận riêng), như vậy còn tối ưu prerender được
    • Nếu đã có công cụ tốt được chuẩn bị sẵn thì không nhất thiết phải deep customize, miễn là không đẩy độ phức tạp triển khai của lựa chọn lên quá cao
    • Cần nhận thức rằng các tính năng quá phức tạp cuối cùng có thể gây hại cho cả người dùng lẫn developer
    • Giống như câu chuyện 640K là đủ, có người nghĩ progressive/partial reads thật sự có thể đóng vai trò lớn với tốc độ UX trong thời đại WASM
    • Nếu các kiểu binary encoding như protobuf có thêm partial read và streaming được định nghĩa rõ ràng, gánh nặng kỹ thuật cho engineer sẽ tăng nhưng UX tạo ra có thể tiến bộ rất mạnh
  • Có quan điểm cho rằng Progressive JPEG là cần thiết do đặc tính file media, nhưng với Text/HTML thì không cần đến vậy; sự phức tạp tăng lên chỉ là hệ quả tự mâu thuẫn của việc bundle JS ngày càng lớn
    • Có người chỉ ra rằng nguyên nhân chậm thực tế không chỉ nằm ở “kích thước” dữ liệu
    • Ngay cả khi truy vấn dữ liệu trên server mất nhiều thời gian hoặc mạng chậm, việc progressive reveal vẫn có ý nghĩa
    • Thay vì đợi dữ liệu hoàn chỉnh toàn bộ, render theo từng giai đoạn với loading UI ở thời điểm thích hợp thực sự có thể cải thiện trải nghiệm người dùng
  • Có ý kiến cho rằng chiến lược tách endpoint vốn đã là lời giải có nhiều ưu điểm (tránh Head of line blocking, cải thiện tùy chọn filter, hỗ trợ live update, tối ưu hiệu năng độc lập, v.v.)
    • Một quan điểm khác là gốc rễ vấn đề nằm ở nỗ lực đối xử với application như một document platform
    • Ứng dụng thực tế không vận hành như một “tài liệu”, và để lấp khoảng cách đó người ta phải thêm rất nhiều code phụ và hạ tầng
    • Hai bài viết dài sau giải thích bổ sung về nhược điểm thực sự của việc dùng endpoint riêng và hướng tiến hóa tiếp theo: https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
    • Tóm lại, endpoint cuối cùng trở thành hợp đồng API “chính thức” giữa server và client, và khi code ngày càng được module hóa thì hiệu năng có thể bị thiệt (như hiện tượng waterfall)
    • Việc gộp các quyết định ở server để xử lý một lần (coalescing) có thể là phương án tốt hơn cả về hiệu năng lẫn độ linh hoạt trong cấu trúc