4 điểm bởi GN⁺ 2026-02-28 | 1 bình luận | Chia sẻ qua WhatsApp
  • Chuẩn Web Streams được thiết kế để truyền dữ liệu theo luồng một cách nhất quán giữa trình duyệt và máy chủ, nhưng hiện nay độ phức tạp và các giới hạn về hiệu năng đang làm giảm trải nghiệm của lập trình viên
  • API hiện tại tạo ra gánh nặng không cần thiết cả trong việc sử dụng lẫn triển khai do các ràng buộc thiết kế như quản lý khóa (lock), BYOB, backpressure
  • Cloudflare đề xuất một mô hình stream mới dựa trên async iteration, và cách tiếp cận này cho thấy hiệu năng nhanh hơn 2 lần đến tối đa 120 lần
  • API mới nâng cao hiệu quả và tính nhất quán thông qua cấu trúc async iterable đơn giản, chính sách backpressure tường minh, và hỗ trợ song song đồng bộ/bất đồng bộ
  • Cách tiếp cận này có thể cho phép một mô hình streaming thống nhất trên mọi runtime như Node.js, Deno, Bun, trình duyệt..., đồng thời có thể trở thành điểm khởi đầu cho các thảo luận tiêu chuẩn trong tương lai

Những giới hạn mang tính cấu trúc của Web Streams

  • Chuẩn WHATWG Streams được phát triển trong giai đoạn 2014~2016 và được thiết kế xoay quanh trình duyệt; vào thời điểm đó async iteration chưa tồn tại, nên đã đưa vào mô hình reader/writer riêng biệt
    • Điều này tạo ra những thủ tục không cần thiết như quản lý khóa, vòng lặp đọc phức tạp, xử lý bộ đệm BYOB
  • Mô hình khóa (locking) chiếm dụng stream theo kiểu độc quyền, ngăn việc tiêu thụ song song; nếu quên releaseLock() thì stream có thể bị khóa vĩnh viễn
  • Tính năng BYOB (Bring Your Own Buffer) nhắm tới việc tái sử dụng bộ nhớ, nhưng do mô hình tách/chuyển bộ đệm phức tạp, nó ít được dùng trong thực tế và khó triển khai
  • Backpressure được hỗ trợ trên lý thuyết, nhưng cấu trúc hiện tại không cho phép kiểm soát thực tế, ví dụ enqueue() vẫn thành công ngay cả khi giá trị desiredSize là số âm
  • Mỗi lần gọi read() đều bắt buộc tạo Promise, khiến các tác vụ streaming tần suất cao bị giảm hiệu năng và tăng tải GC

Các vấn đề bộc lộ trong thực tế

  • Nếu không tiêu thụ phần thân phản hồi của fetch(), có thể xảy ra cạn kiệt connection pool; khi dùng tee(), sẽ phát sinh bộ đệm bộ nhớ không giới hạn
  • TransformStream sẽ xử lý ngay lập tức bất kể đã sẵn sàng để đọc hay chưa, gây ra bùng nổ bộ đệm trong môi trường có bên tiêu thụ chậm
  • Trong server-side rendering (SSR), GC thrashing xảy ra do phải xử lý hàng nghìn chunk nhỏ, khiến hiệu năng sụt giảm mạnh
  • Mỗi runtime (Node.js, Deno, Bun, Workers) đều đã đưa vào các đường tối ưu hóa phi tiêu chuẩn để giảm nhẹ vấn đề này, nhưng điều đó lại làm giảm khả năng tương thích và tính nhất quán
  • Web Platform Tests yêu cầu hơn 70 tệp kiểm thử phức tạp, phản ánh việc quản lý trạng thái nội bộ quá mức và hành vi khó trực quan

Các nguyên tắc thiết kế của Streams API mới

  • Stream được định nghĩa như một async iterable đơn giản, có thể được tiêu thụ trực tiếp bằng for await...of
  • Áp dụng biến đổi pull-through, chỉ xử lý khi bên tiêu thụ thực sự yêu cầu dữ liệu
  • Cung cấp chính sách backpressure tường minh (strict, block, drop-oldest, drop-newest) để ngăn bùng nổ bộ nhớ
  • Truyền dữ liệu theo đơn vị chunk theo lô (Uint8Array[]) để giảm chi phí tạo Promise
  • Được đơn giản hóa thành xử lý chuyên cho byte, loại bỏ BYOB và các khái niệm controller phức tạp
  • Hỗ trợ đường đi đồng bộ (synchronous) để loại bỏ overhead của Promise trong các tác vụ thiên về CPU

Ví dụ và đặc điểm của API mới

  • Có thể dễ dàng tạo cặp writer/readable bằng Stream.push(), và thu thập toàn bộ văn bản bằng Stream.text()
  • Stream.pull() xây dựng pipeline trì hoãn (lazy) và chỉ thực thi tại thời điểm tiêu thụ
  • Stream.share()Stream.broadcast() hỗ trợ quản lý nhiều bên tiêu thụ một cách tường minh
  • API sync/async song song (Stream.pullSync(), Stream.textSync()) tối đa hóa hiệu năng cho các phép toán không có I/O
  • Để tương tác với Web Streams, có thể chuyển đổi thông qua các hàm adapter đơn giản

So sánh hiệu năng và triển vọng

  • Trong benchmark trên Node.js, tốc độ xử lý được xác nhận là nhanh hơn tối đa 80~90 lần; trong trình duyệt có thể nhanh hơn tối đa trên 100 lần
    • Ví dụ: trong chuỗi biến đổi 3 tầng, đạt 275GB/s so với 3GB/s
  • Mức tăng hiệu năng đến từ việc loại bỏ overhead bất đồng bộ, xử lý theo lô và thiết kế dựa trên pull
  • Phần triển khai này được viết hoàn toàn bằng TypeScript/JavaScript, và còn có thể cải thiện thêm nếu được triển khai native
  • Cloudflare đưa cách tiếp cận này ra như điểm khởi đầu cho thảo luận tiêu chuẩn, đồng thời kêu gọi phản hồi từ cộng đồng lập trình viên

Kết luận

  • Web Streams từng là một thiết kế hợp lý trong bối cảnh bị giới hạn lúc đó, nhưng không còn phù hợp với các tính năng ngôn ngữ và mô hình phát triển của JavaScript hiện đại
  • Mô hình mới dựa trên async iterable đáp ứng đồng thời tính đơn giản, hiệu năng và khả năng kiểm soát tường minh, đồng thời mở ra khả năng xây dựng một hệ sinh thái streaming nhất quán giữa các runtime
  • Cloudflare đã công bố bản triển khai tham chiếu, tài liệu và mã ví dụ trên GitHub tại jasnell/new-streams
  • Mục tiêu không phải là lập ra một tiêu chuẩn mới ngay lập tức, mà là tạo một điểm xuất phát thực tế để thảo luận về “một Streams API tốt hơn”

1 bình luận

 
GN⁺ 2026-02-28
Ý kiến trên Hacker News
  • Đã tự thiết kế một giao diện Stream tốt hơn cả API được đề xuất trong bài này
    Đề xuất hiện tại có dạng async iterator of UInt8Array, còn tôi đề xuất một cấu trúc mà next() có thể trả về cả kết quả đồng bộ lẫn bất đồng bộ
    Làm như vậy thì
    có thể duyệt đơn giản hơn bằng một iterator duy nhất so với cấu trúc hiện tại
    nếu áp dụng phép biến đổi đồng bộ cho đầu vào đồng bộ thì toàn bộ quá trình có thể chạy đồng bộ, giúp giảm trùng lặp mã
    giảm việc tạo Promise không cần thiết nên cải thiện hiệu năng
    có thể kiểm soát tính đồng thời, vượt qua giới hạn của async iterator

    • Bạn nói cách bạn đề xuất tốt hơn, nhưng thực ra tôi nghĩ cách của phía kia vượt trội hơn vì nó là một dạng nguyên thủy nền tảng hơn
      Với cách của bạn thì không dễ để dựng lại cấu trúc của họ, còn chiều ngược lại thì được
      Iterator thiên về I/O cần trả về các chunk theo đơn vị T để tránh lãng phí bộ đệm
    • Khái niệm stream được đề xuất khá thú vị, nhưng thiết kế của họ lấy khả năng tương thích với AsyncIterator làm tiền đề
      Lý do dùng Uint8Array là để khớp với byte stream ở cấp hệ điều hành
      Trên thực tế, ngay cả trong các dự án dựa trên C thì cấu trúc như vậy cũng hiệu quả nhất, nên việc để giao thức có thông tin kiểu được xây trên nó là tự nhiên
    • Tôi đã dùng microbenchmark để đo chênh lệch tốc độ giữa gọi hàm đồng bộ và hàm async trên Node 24, và thấy chậm hơn khoảng 90 lần
      Ở các phiên bản cũ hơn thì từng chênh tới 105 lần
      Tôi nhớ đã có tối ưu cho xử lý async ở Node 16, và khi đó một số bài test từng bị hỏng
    • Kiểu Uint8Array không hề tồn tại
      Uint8Array chỉ đơn thuần là một kiểu nguyên thủy biểu diễn mảng byte, còn thông tin kiểu nên được xử lý ở tầng ứng dụng chứ không phải tầng giao thức
    • Cấu trúc này khá giống với khái niệm transducer của Clojure
      Tham khảo: tài liệu về Clojure Transducers
  • Async iterable cũng không phải lời giải hoàn hảo
    Promise và overhead chuyển stack khá lớn nên hiệu năng kém khi xử lý dữ liệu nhỏ
    Trong Lit-SSR, để giải quyết việc này, họ dùng cách nhúng thunk vào iterable đồng bộ
    chỉ khi cần tác vụ async mới gọi thunk và await, nhờ đó cải thiện hiệu năng SSR lên 12~18 lần
    Tuy vậy, Streams API khó chấp nhận kiểu hợp đồng mong manh như thế, nên tôi nghĩ cấu trúc cho phép xử lý bất đồng bộ tùy chọn như write()writeAsync() sẽ là lý tưởng

    • stream iterator của tôi có thể giải quyết vấn đề bạn nói
      Tôi đã chia sẻ ví dụ dùng generator đồng bộ trong mã trên GitHub
      Điểm cốt lõi là đoạn step.value.then(value => this.next(value))
    • Tôi thích đề xuất của conartist6 (next(): {done, value: T} | Promise)
      Từ sau cuộc tranh luận “Do not unleash Zalgo” năm 2013, người ta có xu hướng né dạng MaybeAsync, nhưng
      tôi nghĩ nỗi sợ này bị phóng đại quá mức và đang cản trở thiết kế API nhanh và linh hoạt
      Cũng có thể làm utility để kéo nhiều giá trị cùng lúc, và trên thực tế tôi thấy vấn đề tốc độ của generator không lớn đến vậy
  • Làm việc với Web Streams trong Node.js thật sự rất đau đớn
    Vì được thiết kế xoay quanh trình duyệt nên dùng trong môi trường máy chủ rất bất tiện
    Ngay cả biến đổi đơn giản cũng phải bọc trong transform stream, và khó chain trực quan như .pipe()
    Cách tiếp cận async iterable tự nhiên hơn nhiều và rất hợp với for-await-of
    Đặc tả Web Streams quá thiên về trừu tượng hóa nên kém thực dụng

    • Thật ngạc nhiên là có người thực sự dùng Web Streams trong Node
      Tôi cứ nghĩ nó chỉ để tương thích giữa client và server
  • Lợi ích thật sự không chỉ là hiệu năng mà còn là tính nhất quán giữa các môi trường (convergence)
    Nếu ReadableStream hoạt động giống nhau trên trình duyệt, Worker và các runtime khác
    thì khả năng di chuyển mã sẽ cao hơn và cũng giúp giảm lỗi backpressure
    Việc chuẩn hóa tầng stream là cốt lõi để xây dựng hệ thống streaming đáng tin cậy

    • Đúng vậy, giá trị lớn nằm ở sự chuẩn hóa chứ không chỉ ở hiệu năng
  • Trước đây tôi từng tạo ra một abstraction tên là Repeater
    Đó là ý tưởng chuyển Promise constructor sang async iterable, với việc điều khiển sự kiện bằng push/stop
    Thư viện Repeater đủ ổn định để đạt 6,5 triệu lượt tải mỗi tuần
    Gần đây tôi thiên về streams hơn, nhưng phê bình liên quan đến tee() vẫn còn nguyên giá trị
    Tôi cho rằng hướng đi đúng là xem async iterable như abstraction mặc định

    • Tôi thấy việc stop của Repeater vừa là hàm vừa là Promise khá thú vị
      Sau khi xem mã nguồn
      tôi nghĩ tuy khác với mẫu truyền thống, nhưng đó có thể là lựa chọn có chủ đích để tăng tính công thái học
    • Hơi lạc đề nhưng ví dụ về mã Konami làm tôi thấy rất vui
      Tôi còn hoài niệm đến mức dùng “Up, Up, Down, Down, Left, Right, Left, Right, B, A” trong chữ ký email
  • Tôi cũng từng làm một wrapper để dùng AsyncIterable gọn hơn
    Đó là fluent-async-iterator,
    khá hữu ích cho streaming dữ liệu quy mô nhỏ trong Lambda hay pipeline CLI
    Đến giờ tôi vẫn mong đã có một API tốt hơn xuất hiện

  • Hành vi backpressure của ReadableStream.tee() gây bối rối vì ngược với pipe() của Node.js
    Trong đặc tả ghi rằng “đầu ra chậm nhất phải quyết định tốc độ”, nhưng triển khai thực tế lại bị chặn ngay cả khi phía nhanh hơn chưa được tiêu thụ
    Tôi nghĩ một cấu trúc ngắn gọn dựa trên push như Stream API mới sẽ tốt hơn
    Node và Web Streams dùng hàng đợi vô hạn để có thể gọi res.write() đồng bộ liên tục, nhưng
    API này buộc theo luồng yield dựa trên generator nên an toàn hơn

  • Việc cạn pool kết nối khi dùng undici(fetch) trong Node.js
    là do giới hạn của ngôn ngữ có garbage collection
    Nếu không đóng tài nguyên một cách tường minh thì có thể rò rỉ tùy theo thời điểm GC chạy
    Cách tiếp cận RAII (reference counting) của C++ lại an toàn hơn

  • Về giải phóng tài nguyên, tôi hy vọng mẫu using/await using sẽ ngày càng phổ biến
    Tôi đang áp dụng cấu trúc hỗ trợ dispose/disposeAsync vào driver DB, tương tự using của C#

  • Các con số benchmark (ví dụ: 530GB/s) vượt quá băng thông bộ nhớ của M1 Pro (200GB/s) nên khó đáng tin
    Rất có thể đó là một benchmark kiểu vibe-coded với chất lượng triển khai kém