Web sayfanızı yöneten JavaScript event loop'u, bir Node.js sunucusunu besleyeninden çok farklı davranır ve dikkatli olmazsanız bu uyumsuzluk bir kullanıcı arayüzünü (UI) dondurabilir veya I/O işlemlerini tıkayabilir. Her iki ortamda da çalışan asenkron kodlar yazan herkes için bu ikisinin nerede ayrıştığını bilmek esastır.
Bu ayrım neden önemlidir
Event loop, ECMAScript spesifikasyonu ile tanımlanmaz; host (ev sahibi) ortamda yaşar. Tarayıcılar, kareleri (frames) işlerken bir sayfanın yanıt verebilir kalmasını sağlamak zorundadır; Node.js ise bloklamayan (non-blocking) I/O etrafında inşa edilmiştir. Bir host ortamında çalışan desenleri diğeriyle karıştırmak, yeniden üretilmesi zor hatalara yol açabilir: uzun bir promise zinciri bir tarayıcının yeniden boyama (repaint) işlemini durdurabilirken, kontrol edilmeyen bir process.nextTick döngüsü Node'un I/O aşamalarına hiç ulaşamamasına neden olabilir.
Tarayıcının sıra tabanlı döngüsü
Bir tarayıcıda döngü; görev yürütme, microtask boşaltma ve render işlemlerini birbirine harmanlayan tek bir döngü çalıştırır:
- Bir macrotask çalıştır (bir tıklama işleyicisi, bir
setTimeoutvb.). - Tüm microtask'ları boşalt (promises,
queueMicrotask). - Eğer bir kare (frame) zamanı geldiyse, hedef 60 fps'e ulaşmak için boyama (paint) ve birleştirme (composite) işlemlerini yap.
- Tekrarla.
İki API, geliştiricilere bu döngüye açıkça müdahale etme imkanı tanır:
requestAnimationFrame– Tarayıcı boyama yapmadan hemen önce çağrılır. Animasyon çalışmaları için doğru yerdir çünkü geri çağırma (callback), mevcut microtask'lardan sonra ancak bir sonraki kareden önce çalışır.requestIdleCallback– Tarayıcının yüksek öncelikli bir işi olmadığında çağrılır. Analiz (analytics) veya veri ön yükleme gibi düşük etkili görevler için kullanışlıdır.
Tuzak: microtask starvation
Tarayıcı, render yapmadan önce microtask kuyruğunu boşalttığı için, uzun bir promise zinciri UI'ın hiçbir zaman boyanmamasına neden olabilir. Çağrı yığını (call stack) bloklanmaz; sayfa sadece render adımına asla ulaşamaz, bu da kullanıcıya bir donma gibi hissettirir.
Node'un libuv tabanlı döngüsü
Node.js, döngüsünü işleri her biri kendi kuyruğuna sahip farklı aşamalara bölen bir C kütüphanesi olan libuv'ye devreder:
- Timers –
setTimeoutvesetInterval'den gelen geri çağırmalar. - Pending callbacks – İşletim sistemi düzeyinde zaten tamamlanmış olan ertelenmiş I/O geri çağırmaları.
- Poll – Yeni I/O olaylarını (dosya okuma, ağ verisi) çeker.
- Check –
setImmediategeri çağırmalarını çalıştırır. - Close callbacks – Bir soket veya handle kapandığında tetiklenir.
Bu aşama sırasının dışında kalan iki yapı vardır:
process.nextTick– Mevcut işlem biter bitmez, microtask kuyruğundan önce çalışır.
Tuzak: I/O starvation
Eğer bir fonksiyon, kontrolü bırakmadan (yielding) sürekli olarak process.nextTick planlarsa, Node asla "next-tick" adımının ötesine geçemez. Ağ istekleri, dosya okumaları ve zamanlayıcılar boşta bekler; bu da sunucu tarafında gecikme sıçramalarına veya doğrudan kilitlenmelere neden olur.
Pratikte setImmediate vs. setTimeout
Her ikisi de bir sonraki yineleme için geri çağırmalar planlar, ancak göreceli sıraları nerede çağrıldıklarına bağlıdır:
- Üst düzey kod (Top-level code) – Sıralama garanti değildir; sürecin ne kadar hızlı başladığına bağlıdır.
- Bir I/O geri çağırmasının içinde – Sıralama belirlidir (deterministic):
setImmediate,setTimeout(fn, 0)'dan önce çalışır. Poll aşaması bittikten sonra libuv, sıfır gecikmeli bir zaman aşımı için Timers aşamasına tekrar girmeden önce Check aşamasına (setImmediate'in bulunduğu yer) geçer.
Bu nüans, bir okuma tamamlandıktan hemen sonra bir kaynağı temizlemek gibi hassas bir sıralamaya güvendiğinizde önem kazanır.
Temel farklara hızlı bakış
- Hedef: Tarayıcılar görsel güncellemelere öncelik verir; Node ise I/O hazır olma durumuna öncelik verir.
- Render kancası (hook):
requestAnimationFrame(sadece tarayıcı). - Aşamaya özel kanca (hook):
setImmediate(sadece Node, Check aşamasında tetiklenir). - Yüksek öncelikli kuyruk:
process.nextTick(sadece Node, microtask'lardan önce çalışır). - Starvation riski: Tarayıcılarda uzun promise zincirleri; Node'da sınırsız
process.nextTick.
Bundan sonra nelere dikkat edilmeli
Her iki ortamda da çalışan bir kod tabanını (örneğin izomorfik kütüphaneler) yönetiyorsanız, şu durumların yaşandığı yerleri denetleyin:
- Event loop'a kontrolü bırakmadan (yielding) çok sayıda promise zincirleme. Renderer'a bir şans tanımak için
await new Promise(r => setTimeout(r, 0))ekleyin veya tarayıcıdarequestIdleCallbackkullanın. - Ertelenebilecek işler için
process.nextTickkullanmayın. "Next-tick" aciliyetine ihtiyacınız olmadığındasetImmediateveya normal bir promise tercih edin. setTimeout(fn, 0)vesetImmediate'in birbirinin yerine geçebileceğini varsaymayın. Sıralama önemliyse I/O geri çağırmaları içindeki sıralamayı test edin.
Temel Çıkarım
Event loop, evrensel bir JavaScript özelliği değil, ortama (host) özgü bir zamanlayıcıdır. Tarayıcılar render işlemini döngüye dahil ederken; Node, I/O işlemlerini libuv aşamalarına ayırır. Öncelik mekanizmalarının yanlış kullanımı —tarayıcıda microtask'lar, Node'da process.nextTick— her ortamın hizmet etmek üzere tasarlandığı sistem bileşeninin işleyişini engelleyebilir. Asenkron desenlerinizi host'un döngü modeliyle uyumlu hale getirirseniz, hem donan sayfaların hem de bloke olan sunucuların önüne geçmiş olursunuz.
