Node.js, tek iş parçacıklı (single-threaded) yapısıyla yüksek performanslı sunucu uygulamaları geliştirmeyi mümkün kılan bir platformdur. Bu başarının ardındaki temel mekanizma event loop'dur. Event loop, Node.js'in asenkron işlemleri (I/O işlemleri, zamanlayıcılar, Promise'ler gibi) nasıl yönettiğini anlamak, hem performans hem de hata ayıklama açısından kritik öneme sahiptir. Bu yazıda, event loop'un aşamalarını, microtask ile macrotask arasındaki farkları, sık yapılan hataları ve kodunuzu optimize etmek için ipuçlarını inceleyeceğiz.
Event Loop Nedir ve Neden Önemlidir?
Node.js, libuv kütüphanesi tarafından sağlanan event loop ile asenkron işlemleri yönetir. Event loop, JavaScript kodunu sırayla çalıştıran bir döngüdür, ancak bloklayıcı olmayan I/O işlemlerini (dosya okuma, ağ istekleri gibi) arka planda gerçekleştirir ve tamamlandıklarında callback'leri uygun sırayla çalıştırır. Bu mekanizma, tek iş parçacıklı bir dilde yüksek eşzamanlılık sağlar.
Event loop, Node.js'in kalbidir. Onu anlamadan asenkron kodun davranışını tahmin etmek mümkün değildir. Microtask ve macrotask kavramları, callback'lerin hangi sırayla çalıştırılacağını belirler.
Event Loop'un Aşamaları
Event loop altı aşamadan oluşur. Her aşamada belirli görev türleri işlenir:
- timers:
setTimeoutvesetIntervalcallback'lerinin süresi dolduysa çalıştırılır. - pending callbacks: Bazı sistem hata callback'leri (örneğin TCP hataları) bu aşamada işlenir.
- idle, prepare: Dahili kullanım, genellikle görmezden gelinir.
- poll: Yeni I/O olaylarını alır, I/O callback'lerini çalıştırır. Eğer başka bir iş yoksa bu aşamada bekleyebilir.
- check:
setImmediatecallback'leri bu aşamada çalıştırılır. - close callbacks: Kapatılan bağlantıların (örneğin socket) callback'leri çalıştırılır.
Her aşamadan sonra, microtask kuyruğu (Promise callback'leri, queueMicrotask, process.nextTick) işlenir. process.nextTick aslında bir microtask değildir, ancak teknik olarak event loop'un her aşaması arasında öncelikle işlenen ayrı bir kuyruğa sahiptir.
Microtask ve Macrotask Farkları
Macrotask'lar event loop'un her bir aşamasında işlenen görevlerdir: setTimeout, setInterval, I/O callback'leri, setImmediate. Microtask'lar ise Promise'ler (.then, .catch), async/await sonrasındaki kod blokları, MutationObserver ve queueMicrotask ile eklenen görevlerdir.
Kritik nokta: Event loop bir macrotask'ı işlemeye başlamadan önce mevcut tüm microtask'ları tamamlar. Bu, aşağıdaki gibi beklenmedik davranışlara yol açabilir:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Çıktı: 1, 4, 3, 2 (Promise microtask'ı setTimeout'tan önce)
Buradaki zamanlama farkı, microtask kuyruğunun macrotask'tan önce işlenmesinden kaynaklanır. Bu mekanizmayı anlamak, özellikle CommonJS vs ES modülleri arasında geçiş yaparken modül yüklemenin asenkron etkilerini değerlendirmek için önemlidir.
process.nextTick vs Promise vs setImmediate
Üç farklı asenkron mekanizma arasındaki farklar:
| Mekanizma | Kuyruk Türü | Çalıştırılma Zamanı | Kullanım Amacı |
|---|---|---|---|
process.nextTick | nextTick kuyruğu | Her aşama arasında, microtask'lardan önce | Mevcut işlemin hemen ardından, ancak event loop'un bir sonraki aşamasından önce çalıştırılmalı |
| Promise (microtask) | Microtask kuyruğu | Her aşamanın sonunda, nextTick'ten sonra | Asenkron işlem sonuçlarını işleme |
setImmediate | Macrotask (check aşaması) | Bir sonraki event loop döngüsünde | I/O işlemlerinden sonra, ancak setTimeout'dan önce (genelde) |
Önemli: process.nextTick aynı döngüde microtask'lardan bile önce çalışır. Ancak I/O işlemlerini ertelemek gibi bir yan etkisi vardır; bu yüzden resmi dokümanlar genellikle setImmediate kullanılmasını önerir. Aslında event loop'un setImmediate'i check aşamasında işlemesi, I/O callback'lerinden sonra gelir ve daha öngörülebilirdir.
Sık Yapılan Hatalar ve Bunlardan Kaçınma Yolları
- Zamanlama bağımlılığı:
setTimeout(fn, 0)ilesetImmediate'in hangisinin önce çalışacağı ana iş parçacığının yüküne bağlıdır. Kesin sıralama istiyorsanızsetImmediatekullanın. - nextTick ile aşırı kuyruk oluşturma: Döngüsel
process.nextTickçağrıları, I/O işlemlerini aç bırakabilir. Bunun yerinesetImmediatetercih edilmelidir. - Promise içinde senkron hata fırlatma:
new Promise(...)içinde hata fırlatırsanız, yakalanmazsaunhandledRejectionolayına dönüşür. Her zaman.catchekleyin. - Async fonksiyonlarda gereksiz await: Paralel çalışabilecek işlemleri sıraya sokmayın;
Promise.allkullanın.
Bu hataları fark etmek ve düzeltmek, özellikle JWT kimlik doğrulama gibi akışlarda doğrulama ve token yenileme işlemlerinin asenkron yönetimini iyileştirir.
Performans İpuçları
Event loop'u bloke etmekten kaçının. Uzun süreli CPU işlemlerini (örneğin, büyük veri setlerini işleme) ana iş parçacığında yapmayın. Bunun yerine Worker Threads veya child process kullanın. Ayrıca aşağıdaki noktalara dikkat edin:
- Asenkron I/O kullanın: Dosya okuma/yazma, ağ istekleri gibi işlemlerde callback, Promise veya async/await kullanın.
- Zamanlayıcıları verimli kullanın:
setTimeoutvesetIntervaluzun gecikmelerle ayarlanmışsa, zamanlayıcıyı iptal etmek (clearTimeout) hafıza sızıntılarını önler. - Promise kuyruğunu kontrol edin: Çok fazla microtask oluşturmak, macrotask'ların (özellikle I/O) gecikmesine neden olabilir. Dengeli bir asenkron tasarım hedefleyin.
- setImmediate kullanımı: I/O sonrası hemen çalıştırılması gereken işlemler için
setImmediatedaha uygundur. Ayrıca REST API versiyonlama gibi senaryolarda, gelen istekleri işleme sırasını öngörülebilir kılar.
Async/Await ile Microtask ve Macrotask İlişkisi
Async fonksiyonlar, await ifadesine geldiklerinde, kalan kod bir Promise zincirine dönüşür ve microtask olarak kuyruğa eklenir. Bu, şu anlama gelir: await sonrasındaki kod, çağrıldığı senkron bağlamın hemen ardından değil, mevcut macrotask tamamlandıktan sonra, diğer microtask'larla birlikte işlenir. Aşağıdaki örnek bunu gösterir:
async function test() {
console.log('Başla');
await Promise.resolve();
console.log('Microtask');
setTimeout(() => console.log('Macrotask'), 0);
console.log('Devam');
}
test();
console.log('Senkron');
// Çıktı: Başla, Senkron, Microtask, Devam, Macrotask
Bu davranış, özellikle React Native push notification gibi mobil uygulamalarda, bildirim işleme sırasını etkileyebilir; bu yüzden asenkron akışı iyi anlamak gerekir.
Sonuç
Event loop, Node.js'in en güçlü ve en yanlış anlaşılan kavramlarından biridir. Microtask ve macrotask ayrımını öğrenmek, asenkron kodunuzun nasıl davranacağını tahmin etmenizi ve performans sorunlarını gidermenizi sağlar. Bu bilgileri uygulayarak, daha sağlam ve verimli Node.js uygulamaları geliştirebilirsiniz.
Sık Sorulan Sorular
Node.js'de microtask ve macrotask nedir?
Microtask'lar Promise, async/await ve queueMicrotask ile oluşturulan görevlerdir. Macrotask'lar ise setTimeout, setInterval, I/O callback'leri ve setImmediate gibi daha büyük görevlerdir. Event loop her macrotask'tan sonra tüm microtask'ları işler.
process.nextTick neden tehlikeli olabilir?
process.nextTick, event loop'un her aşaması arasında ve microtask'lardan önce çalışır. Döngüsel kullanımı, I/O işlemlerinin sürekli ertelenmesine neden olarak uygulamanın performansını düşürebilir. Bunun yerine setImmediate tercih edilmelidir.
setTimeout(fn, 0) ile setImmediate arasındaki fark nedir?
setTimeout(fn, 0) timers aşamasında, setImmediate ise check aşamasında çalışır. Ana iş parçacığı boşsa, hangisinin önce çalışacağı belirsizdir. Kesin sıralama istiyorsanız setImmediate kullanın. I/O callback'leri içinde setImmediate her zaman setTimeout'tan önce çalışır.
Await ifadesi nasıl çalışır ve event loop'u nasıl etkiler?
Await, bir Promise tamamlanana kadar async fonksiyonun yürütmesini askıya alır. Fonksiyonun geri kalanı microtask olarak kuyruğa eklenir. Bu, mevcut macrotask'ın tamamlanmasını beklemeden diğer microtask'larla birlikte işlenir.
Event loop'u bloke etmekten nasıl kaçınabilirim?
Uzun süreli CPU işlemlerini Worker Threads veya child process'lere taşıyın. Asenkron I/O API'lerini kullanın, aşırı process.nextTick'ten kaçının ve Promise kuyruğunu kontrol altında tutun.






