- Standar Web Streams dirancang untuk streaming data yang konsisten antara browser dan server, tetapi saat ini pengalaman pengembang menurun karena kompleksitas dan keterbatasan performa
- API yang ada menimbulkan beban yang tidak perlu baik dalam penggunaan maupun implementasi karena batasan desain seperti pengelolaan lock, BYOB, dan backpressure
- Cloudflare mengusulkan model stream baru berbasis iterasi asinkron (async iteration), dan pendekatan ini menunjukkan performa 2x hingga 120x lebih cepat
- API baru meningkatkan efisiensi dan konsistensi melalui struktur async iterable yang sederhana, kebijakan backpressure yang eksplisit, dan dukungan jalur sinkron/asinkron secara bersamaan
- Pendekatan ini memungkinkan model streaming terpadu di semua runtime seperti Node.js, Deno, Bun, dan browser, serta dapat menjadi titik awal untuk diskusi standar di masa depan
Keterbatasan struktural Web Streams
- Standar WHATWG Streams dikembangkan pada 2014~2016 dan dirancang dengan fokus pada browser; saat itu async iteration belum ada, sehingga model reader/writer terpisah diperkenalkan
- Akibatnya muncul prosedur yang tidak perlu seperti pengelolaan lock, loop pembacaan yang rumit, dan penanganan buffer BYOB
- Model lock membuat stream dikuasai secara eksklusif sehingga mencegah konsumsi paralel, dan jika
releaseLock() terlewat maka stream dapat terkunci permanen
- Fitur BYOB (Bring Your Own Buffer) ditujukan untuk penggunaan ulang memori, tetapi model pemisahan dan transfer buffer yang rumit membuat pemakaian nyatanya rendah dan implementasinya sulit
- Backpressure secara teori didukung, tetapi strukturnya tidak memungkinkan kontrol nyata, misalnya
enqueue() tetap berhasil walau nilai desiredSize negatif
- Setiap pemanggilan
read() memaksa pembuatan Promise, sehingga pada streaming frekuensi tinggi hal ini menyebabkan penurunan performa dan beban GC
Masalah yang muncul di praktik
- Jika body respons
fetch() tidak dikonsumsi, dapat terjadi kehabisan connection pool, dan saat tee() digunakan muncul buffering memori tanpa batas
TransformStream langsung memproses tanpa memedulikan kesiapan baca, sehingga pada lingkungan dengan konsumen lambat dapat menyebabkan lonjakan buffer
- Dalam server-side rendering (SSR), GC thrashing akibat pemrosesan ribuan chunk kecil membuat performa turun tajam
- Untuk meredam hal ini, tiap runtime (Node.js, Deno, Bun, Workers) memperkenalkan jalur optimasi non-standar, tetapi akibatnya kompatibilitas dan konsistensi menurun
- Web Platform Tests memerlukan lebih dari 70 file pengujian yang kompleks, dan ini merupakan hasil dari pengelolaan state internal yang berlebihan serta perilaku yang tidak intuitif
Prinsip desain Streams API baru
- Stream didefinisikan sebagai async iterable sederhana, sehingga dapat langsung dikonsumsi dengan
for await...of
- Mengadopsi transformasi pull-through agar pemrosesan hanya dilakukan ketika konsumen meminta data
- Menyediakan kebijakan backpressure eksplisit (
strict, block, drop-oldest, drop-newest) untuk mencegah ledakan memori
- Data dikirim dalam satuan chunk batch (
Uint8Array[]) untuk mengurangi biaya pembuatan Promise
- Disederhanakan menjadi pemrosesan khusus byte, dengan menghapus BYOB maupun konsep controller yang rumit
- Dukungan jalur sinkron menghilangkan overhead Promise pada pekerjaan yang berfokus pada CPU
Contoh dan karakteristik API baru
- Dengan
Stream.push() dapat dibuat pasangan writer/readable secara sederhana, dan Stream.text() dapat mengumpulkan seluruh teks
Stream.pull() membentuk pipeline lazy yang hanya dieksekusi saat data dikonsumsi
Stream.share() dan Stream.broadcast() mendukung pengelolaan multi-konsumen yang eksplisit
- API sinkron/asinkron paralel (
Stream.pullSync(), Stream.textSync()) memaksimalkan performa pada operasi tanpa I/O
- Demi interoperabilitas dengan Web Streams, konversi dimungkinkan melalui fungsi adapter sederhana
Perbandingan performa dan prospek
- Dalam benchmark berbasis Node.js, terkonfirmasi kecepatan pemrosesan hingga 80~90x lebih cepat, dan di browser lebih dari 100x lebih cepat
- Contoh: pada rantai transformasi 3 tahap, 275GB/s vs 3GB/s
- Peningkatan performa berasal dari penghapusan overhead asinkron, pemrosesan batch, dan desain berbasis pull
- Implementasi ini ditulis sepenuhnya dalam TypeScript/JavaScript, dan masih ada potensi peningkatan tambahan jika dibuat secara native
- Cloudflare memosisikan pendekatan ini sebagai titik awal diskusi standar dan meminta masukan dari komunitas pengembang
Kesimpulan
- Web Streams masuk akal dalam keterbatasan pada masanya, tetapi tidak lagi sesuai dengan fitur bahasa dan pola pengembangan JavaScript modern
- Model baru berbasis async iterable memenuhi kesederhanaan, performa, dan kontrol eksplisit sekaligus, serta membuka kemungkinan ekosistem streaming yang konsisten lintas runtime
- Cloudflare merilis implementasi referensi, dokumentasi, dan contoh kode di GitHub jasnell/new-streams
- Tujuannya bukan menetapkan standar baru, melainkan menyediakan titik awal yang nyata untuk membahas “Streams API yang lebih baik”
1 komentar
Komentar Hacker News
Saya pernah merancang sendiri antarmuka Stream yang lebih baik daripada API yang diusulkan dalam tulisan ini
Usulan yang ada berbentuk
async iterator of UInt8Array, tetapi saya mengusulkan struktur di mananext()dapat mengembalikan hasil sinkron maupun asinkronDengan begitu
iterasi bisa dilakukan lebih sederhana dengan satu iterator dibanding struktur yang ada
jika input sinkron diproses dengan transformasi sinkron, seluruh pemrosesan bisa tetap sinkron sehingga duplikasi kode berkurang
pembuatan Promise yang tidak perlu berkurang sehingga performa meningkat
kontrol konkurensi juga dimungkinkan sehingga keterbatasan async iterator bisa diatasi
Dengan pendekatanmu, struktur mereka tidak mudah dibangun, sedangkan sebaliknya bisa
iterator yang berpusat pada I/O harus mengembalikan chunk dalam satuan T agar pemborosan buffer bisa dicegah
Alasan menggunakan
Uint8Arrayadalah agar selaras dengan byte stream tingkat OSBahkan pada proyek berbasis C, struktur seperti ini memang paling efisien, jadi protokol dengan informasi tipe secara alami lebih cocok dibangun di atasnya
Pada versi lama, selisihnya bahkan sampai 105 kali
Saya ingat ada optimisasi async di Node 16, dan saat itu beberapa pengujian sempat rusak
Uint8Arrayitu memang adaUint8Arrayhanyalah tipe primitif untuk merepresentasikan array byte, dan informasi tipe harus ditangani di level aplikasi, bukan level protokolReferensi: dokumentasi Clojure Transducers
Async iterable juga bukan solusi sempurna
Overhead Promise dan perpindahan stack besar, sehingga performanya buruk saat menangani data berukuran kecil
Di Lit-SSR, untuk mengatasi ini digunakan pendekatan memasukkan thunk ke dalam iterable sinkron
thunk dipanggil dan di-
awaithanya ketika pekerjaan async benar-benar diperlukan, sehingga performa SSR meningkat 12 hingga 18 kaliNamun Streams API sulit mengadopsi kontrak yang rapuh seperti ini, jadi menurut saya struktur seperti
write()danwriteAsync()yang memungkinkan pemrosesan async opsional akan lebih idealSaya membagikan contoh yang memanfaatkan generator sinkron di kode GitHub
Intinya ada pada bagian
step.value.then(value => this.next(value))next(): {done, value: T} | Promise)Sejak perdebatan “Do not unleash Zalgo” pada 2013, ada kecenderungan menghindari bentuk
MaybeAsync, tetapimenurut saya ketakutan ini terlalu dibesar-besarkan dan malah menghalangi desain API yang cepat dan fleksibel
Kita juga bisa membuat utilitas untuk menarik banyak nilai sekaligus, dan menurut saya masalah kecepatan generator dalam praktiknya tidak terlalu besar
Menangani Web Streams di Node.js itu menyakitkan
Karena dirancang dengan fokus browser, di lingkungan server terasa tidak nyaman
Bahkan untuk transformasi sederhana pun kita harus membungkus transform stream, dan chaining intuitif seperti
.pipe()sulit dilakukanPendekatan async iterable jauh lebih alami dan cocok dengan
for-await-ofSpesifikasi Web Streams terlalu berorientasi abstraksi sehingga kurang praktis
Saya pikir itu hanya untuk kompatibilitas antara klien dan server
Keuntungan sebenarnya bukan cuma performa, tetapi juga konsistensi lintas lingkungan (convergence)
Jika ReadableStream berperilaku sama di browser, Worker, dan runtime lain
portabilitas kode meningkat dan bug backpressure juga berkurang
Standardisasi lapisan stream adalah kunci untuk membangun sistem streaming yang andal
Dulu saya pernah membuat abstraksi bernama Repeater
Konsepnya seperti memindahkan konstruktor Promise ke async iterable, dengan event dikendalikan lewat push/stop
Pustaka Repeater cukup stabil sampai mencatat 6,5 juta unduhan per minggu
Akhir-akhir ini saya lebih suka streams, tetapi kritik terkait
tee()masih tetap validSaya rasa arah yang tepat adalah menjadikan async iterable sebagai abstraksi dasar
stopdi Repeater bertindak sebagai fungsi sekaligus PromiseSetelah melihat kode sumber
saya merasa meski berbeda dari pola tradisional, itu mungkin pilihan yang disengaja demi desain yang ergonomis
Saya bahkan cukup bernostalgia sampai menulis “Up, Up, Down, Down, Left, Right, Left, Right, B, A” di tanda tangan email
Saya juga pernah membuat wrapper agar AsyncIterable bisa dipakai lebih ringkas
Namanya fluent-async-iterator,
dan itu berguna untuk streaming data skala kecil di Lambda atau pipeline CLI
Saya berharap sekarang sudah ada API yang lebih baik
Perilaku backpressure pada
ReadableStream.tee()membingungkan karena berlawanan denganpipe()di Node.jsDi spesifikasinya tertulis bahwa “output paling lambat harus menentukan lajunya”, tetapi implementasi nyata justru tersendat walau sisi yang cepat tidak dikonsumsi
Menurut saya struktur ringkas berbasis push seperti Stream API baru akan lebih baik
Node dan Web Streams memakai antrean tak terbatas sehingga
res.write()bisa dipanggil terus secara sinkron, tetapiAPI ini memaksa alur yield berbasis generator sehingga lebih aman
Masalah habisnya connection pool saat memakai undici(fetch) di Node.js
terjadi karena keterbatasan bahasa dengan garbage collection
Jika resource tidak ditutup secara eksplisit, kebocoran bisa terjadi tergantung timing GC
Pendekatan RAII (reference counting) di C++ justru lebih aman
Untuk pelepasan resource, saya berharap pola
using/await usingmakin luas diadopsiSaya sedang menerapkan struktur yang mendukung dispose/disposeAsync seperti
usingdi C# pada driver databaseAngka benchmark (misalnya 530GB/s) sulit dipercaya karena melampaui bandwidth memori M1 Pro (200GB/s)
Kemungkinan besar itu benchmark vibe-coded dengan kontrol kualitas implementasi yang buruk