- Web Streams standardı, tarayıcı ile sunucu arasında tutarlı veri akışı için tasarlandı; ancak bugün karmaşıklık ve performans sınırlamaları nedeniyle geliştirici deneyimini düşürüyor
- Mevcut API, lock yönetimi, BYOB, backpressure gibi tasarımsal kısıtlar yüzünden hem kullanımda hem uygulamada gereksiz yük oluşturuyor
- Cloudflare, async iteration tabanlı yeni bir stream modeli öneriyor ve bu yaklaşımın 2 kat ile 120 kata kadar daha hızlı performans gösterdiğini belirtiyor
- Yeni API, basit bir async iterable yapısı, açık backpressure politikaları, senkron/asenkron birlikte çalışma desteği ile verimliliği ve tutarlılığı artırıyor
- Bu yaklaşım, Node.js, Deno, Bun, tarayıcılar gibi tüm runtime’larda birleşik bir streaming modeli mümkün kılabilir ve gelecekteki standart tartışmaları için başlangıç noktası olabilir
Web Streams’in yapısal sınırlamaları
- WHATWG Streams standardı 2014–2016 arasında geliştirildi ve tarayıcı merkezli tasarlandı; o dönemde async iteration henüz olmadığından ayrı bir reader/writer modeli benimsendi
- Bunun sonucu olarak lock yönetimi, karmaşık okuma döngüleri, BYOB buffer işleme gibi gereksiz prosedürler ortaya çıktı
- Locking modeli, stream’i tekelleştirerek paralel tüketimi engelliyor;
releaseLock() unutulduğunda stream’in kalıcı olarak kilitli kalması sorunu yaşanabiliyor
- BYOB (Bring Your Own Buffer) özelliği bellek yeniden kullanımını hedefliyordu; ancak karmaşık buffer ayırma ve aktarım modeli nedeniyle pratikte az kullanılıyor ve uygulanması zor
- Backpressure teoride desteklense de
desiredSize değeri negatif olduğunda bile enqueue() başarılı olabildiği için gerçek denetim sağlayamayan bir yapı sunuyor
- Her
read() çağrısında Promise oluşturulması zorunlu olduğundan, yüksek frekanslı streaming’de performans düşüşü ve GC yükü yaratıyor
Pratikte ortaya çıkan sorunlar
fetch() yanıt gövdesi tüketilmediğinde bağlantı havuzunun tükenmesi yaşanıyor; tee() kullanıldığında ise sınırsız bellek buffer’lama oluşabiliyor
TransformStream, okumaya hazır olup olmadığına bakmadan hemen işlem yaptığı için yavaş tüketici ortamlarında buffer patlamasına yol açıyor
- Sunucu tarafı render (SSR) senaryolarında binlerce küçük chunk işlenmesinin yarattığı GC thrashing nedeniyle performans ciddi biçimde düşüyor
- Her runtime (Node.js, Deno, Bun, Workers) bunu hafifletmek için standart dışı optimizasyon yolları ekledi; ancak bu da uyumluluk ve tutarlılık kaybına neden oldu
- Web Platform Tests, 70’ten fazla karmaşık test dosyası gerektiriyor; bu da aşırı iç durum yönetimi ve sezgisel olmayan davranışların sonucu olarak görülüyor
Yeni stream API tasarım ilkeleri
- Stream, basit bir async iterable olarak tanımlanıyor; böylece
for await...of ile doğrudan tüketilebiliyor
- Pull-through dönüşüm benimsenerek işleme yalnızca tüketici veri istediğinde yapılıyor
- Açık backpressure politikaları (strict, block, drop-oldest, drop-newest) sunularak bellek taşması önleniyor
- Veri toplu chunk’lar (
Uint8Array[]) halinde iletilerek Promise üretim maliyeti azaltılıyor
- Yapı, yalnızca byte odaklı işleme ile sadeleştiriliyor; BYOB ve karmaşık controller kavramları kaldırılıyor
- Senkron (synchronous) yol desteği, CPU ağırlıklı işlerde Promise ek yükünü ortadan kaldırıyor
Yeni API’nin örnekleri ve özellikleri
Stream.push() ile kolayca writer/readable çifti oluşturulabiliyor, Stream.text() ile tüm metin toplanabiliyor
Stream.pull(), yalnızca tüketim anında çalışan lazy pipeline yapıları kuruyor
Stream.share() ve Stream.broadcast(), açık çoklu tüketici yönetimi sağlıyor
- Sync/Async birlikte çalışma API’leri (
Stream.pullSync(), Stream.textSync()) sayesinde I/O içermeyen işlemlerde performans en üst düzeye çıkarılıyor
- Web Streams ile birlikte çalışabilirlik için basit adapter fonksiyonlarıyla dönüşüm yapılabiliyor
Performans karşılaştırması ve görünüm
- Node.js tabanlı benchmark’larda 80–90 kata kadar, tarayıcılarda ise 100 katın üzerinde daha hızlı işlem görüldü
- Örnek: 3 aşamalı dönüşüm zincirinde 275GB/s vs 3GB/s
- Performans artışı, asenkron ek yükün kaldırılması, toplu işleme, pull tabanlı tasarım sayesinde elde ediliyor
- Bu uygulama saf TypeScript/JavaScript ile yazıldı; native uygulamada ek iyileştirme potansiyeli bulunuyor
- Cloudflare, bu yaklaşımı standart tartışmaları için bir başlangıç noktası olarak sunuyor ve geliştirici topluluğundan geri bildirim istiyor
Sonuç
- Web Streams, ortaya çıktığı dönemin kısıtları içinde makuldü; ancak modern JavaScript’in dil özellikleri ve geliştirme kalıplarıyla artık uyumlu değil
- Async iterable tabanlı yeni model, sadelik, performans ve açık denetimi birlikte sağlarken runtime’lar arasında tutarlı bir streaming ekosistemi kurma potansiyeli sunuyor
- Cloudflare, GitHub’daki jasnell/new-streams deposunda referans uygulama, dokümantasyon ve örnek kodları paylaşıyor
- Hedef yeni bir standart dayatmak değil; “daha iyi bir stream API’si” tartışmasını başlatacak somut bir çıkış noktası oluşturmak
1 yorum
Hacker News görüşleri
Bu yazıda önerilen API'den daha iyi bir Stream arayüzü bizzat tasarladım
Mevcut öneri
async iterator of UInt8Arraybiçiminde; ben isenext()çağrısının hem senkron hem de asenkron sonuç döndürebildiği bir yapı öneriyorumBöylece
mevcut yapıya kıyasla tek bir iterator ile daha basit dolaşım mümkün oluyor
senkron girdiye senkron dönüşüm uygulanırsa tüm işlem senkron yürütülebiliyor ve kod tekrarını azaltıyor
gereksiz Promise üretimi azaldığı için performans artışı sağlıyor
eşzamanlılık kontrolü mümkün olduğu için async iterator'un sınırlamalarını aşabiliyor
Senin yaklaşımından onların yapısını kolayca kurmak mümkün değil, tersiyse mümkün
I/O odaklı iterator'ların T biriminde chunk döndürmesi, buffer israfını önlemek için gerekli
Uint8Arraykullanılmasının nedeni OS düzeyindeki byte stream'lerle uyum sağlamakGerçekte C tabanlı projelerde de bu yapı en verimli olanı, bu yüzden tür bilgisi taşıyan protokollerin bunun üzerine inşa edilmesi daha doğal
Eski sürümlerde fark 105 kata kadar çıkıyordu
async işleme optimizasyonu Node 16'da gelmişti ve o sırada bazı testlerin bozulduğunu hatırlıyorum
Uint8Arraydiye bir tür yokUint8Arraysadece byte dizisini ifade eden ilkel bir tür ve tür bilgisi protokol değil uygulama seviyesinde ele alınmalıBkz: Clojure Transducers dokümanı
Async iterable da kusursuz bir çözüm değil
Promise ve stack geçişi overhead'i yüksek olduğu için küçük veri parçalarıyla çalışırken performansı kötü
Lit-SSR bunu çözmek için senkron iterable içinde thunk barındıran bir yaklaşım kullandı
async iş gerektiğinde yalnızca thunk çağrılıp await ediliyor; bu da SSR performansını 12 ila 18 kat artırdı
Ancak Streams API'nin böyle kırılgan bir sözleşme yapısını benimsemesi zor olduğundan,
write()vewriteAsync()gibi isteğe bağlı asenkron işleme izin veren bir yapı ideal olurdu diye düşünüyorumSenkron generator kullanan bir örneği GitHub kodu olarak paylaşıyorum
Kilit nokta
step.value.then(value => this.next(value))kısmınext(): {done, value: T} | Promise) beğendim2013'teki “Do not unleash Zalgo” tartışmasından sonra
MaybeAsyncbiçiminden kaçınma eğilimi vardı amabu korkunun aşırı büyütüldüğünü ve hızlı, esnek API tasarımını engellediğini düşünüyorum
Birden fazla değeri tek seferde çekebilen yardımcı araçlar da yapılabilir ve generator hız sorununun pratikte o kadar büyük olmadığını hissediyorum
Node.js'te Web Streams ile çalışmak acı verici
Tarayıcı merkezli tasarlandığı için sunucu ortamında kullanışsız
Basit bir dönüşüm için bile bir transform stream sarmalamak gerekiyor ve
.pipe()gibi sezgisel zincirleme zorAsync iterable yaklaşımı çok daha doğal ve
for-await-ofile iyi örtüşüyorWeb Streams spesifikasyonu fazlasıyla soyutlama odaklı olduğu için pratik değeri düşük
Ben bunun sadece istemci-sunucu uyumluluğu için var olduğunu sanıyordum
Asıl avantaj sadece performans değil, ortamlar arası tutarlılık (convergence)
ReadableStream tarayıcıda, Worker'da ve diğer runtime'larda aynı şekilde çalışırsa
kod taşınabilirliği artar ve backpressure hataları azalır
stream katmanının standardize edilmesi, güvenilir streaming sistemleri kurmanın anahtarı
Eskiden Repeater adında bir soyutlama yapmıştım
Bu, Promise constructor kavramının async iterable'a taşınmış hali; event'ler push/stop ile kontrol ediliyor
Repeater kütüphanesi haftada 6,5 milyon indirmeye ulaşacak kadar stabil
Son dönemde streams'i daha çok tercih ediyorum ama
tee()ile ilgili eleştiriler hâlâ geçerliasync iterable'ı temel soyutlama yapmak doğru yön gibi geliyor
stopun hem fonksiyon hem de Promise gibi davranması ilgimi çekmiştiKaynak kodu inceledikten sonra
bunun geleneksel kalıplardan farklı olsa da ergonomik tasarım için bilinçli bir tercih olabileceğini düşündüm
E-posta imzamda bile “Up, Up, Down, Down, Left, Right, Left, Right, B, A” yazacak kadar nostaljik bağım var
Ben de AsyncIterable'ı daha özlü kullanmak için bir sarmalayıcı yazmıştım
fluent-async-iterator adını vermiştim,
Lambda veya CLI pipeline'larında küçük ölçekli veri akışlarında işe yarıyordu
Şimdiye kadar daha iyi bir API çıkmış olmasını umuyordum
ReadableStream.tee()'nin backpressure davranışı, Node.js'tekipipe()ın tersine işlediği için kafa karıştırıcıSpesifikasyonda “hızı en yavaş çıktı belirlemeli” deniyor ama gerçek uygulamada hızlı taraf tüketilmese bile akış tıkanıyor
Yeni bir Stream API'sindeki gibi push tabanlı, daha sade bir yapı daha iyi olur diye düşünüyorum
Node ve Web Streams sonsuz kuyruklar kullanarak senkron şekilde
res.write()çağrılarının arka arkaya yapılmasına izin veriyor amabu API generator tabanlı yield akışını zorunlu kılarak daha güvenli oluyor
Node.js'te undici(fetch) kullanırken bağlantı havuzunun tükenmesi sorunu yaşanmasının nedeni
çöp toplayıcılı dillerin sınırları
Kaynağı açıkça kapatmazsan GC zamanlamasına bağlı sızıntılar oluşuyor
C++'taki RAII(reference counting) yaklaşımı bu açıdan daha güvenli
Kaynak serbest bırakma konusunda
using/await usingkalıbının giderek yaygınlaşmasını umuyorumC#'taki
usinggibi dispose/disposeAsync destekleyen yapıyı DB driver'lara uyguluyorumBenchmark rakamları (ör. 530GB/s), M1 Pro'nun bellek bant genişliğini (200GB/s) aştığı için güvenilir görünmüyor
Uygulama kalite kontrolü zayıf bir vibe-coded benchmark olma ihtimali yüksek