Web Geliştirme25.07.20267 dk okuma13

Tarayıcılar JavaScript’i Nasıl İşletir? (Bellek Mimarisi ve Call Stack)

arayıcılar JavaScript kodumuzu RAM üzerinde nasıl saklar? Primitive ve Reference tipler arasındaki farkı donanım seviyesinde inceliyor; Stack ve Heap bellek mimarisini, bellek adresleme (pointer) mantığını ve const ile tanımlanan nesnelerin neden değiştirilebildiğini örneklerle masaya yatırıyoruz.

#javascript#browser-engine#stack-vs-heap#bellek-yonetimi#v8-engine#frontend-mimarisi

Serimizin ilk makalesinde sunucu tarafına (Node.js) odaklanmış; I/O işlemlerinin bloklanmaması, C10K krizleri ve donanım seviyesindeki gecikmeler üzerinden backend mimarisini incelemiştik. Şimdi ise yönümüzü istemci tarafına (Client-Side / Tarayıcı dünyası) çeviriyoruz.

Modern tarayıcılar, Masaüstü ya da Mobil cihazlarımızda çalışır ve doğrudan cihazın işlemcisini (CPU) ve belleğini (RAM) kullanır. Donanım seviyesindeki temel kurallar (CPU çekirdekleri, register'lar, bellek erişim süreleri) burada da aynen geçerlidir.

Peki, sunucudaki JavaScript çalıştırma mantığı ile tarayıcıdaki çalışma mantığı arasındaki temel fark nedir?

1. Sunucu vs. Tarayıcı: Odak Noktası Neden Değişir?

Sunucu tarafındaki (Node.js) ana amaç, aynı anda gelen binlerce farklı kullanıcının I/O isteğini (veritabanı, dosya okuma, network) sunucuyu kilitlediği bir krize sokmadan yönetmektir.

Tarayıcı tarafında ise iş tamamen değişir. Tarayıcının ana görevi, yalnızca tek bir kullanıcının etkileşimlerini (klavye, fare tıklamaları, kaydırma hareketleri) anında işlemek ve ekranda akıcı bir görüntü (UI - User Interface) sunmaktır.

Tarayıcıda JavaScript kodları çalışırken şu üç temel süreç yönetilir:

  1. Senkron Kodların İşletilmesi: Sadece bellek (RAM) ve işlemciyi (CPU) kullanan matematiksel hesaplamalar ve değişken tanımlamaları.

  2. Asenkron DOM ve Network Olayları: Kullanıcının tıklamaları (onClick), sayfa kaydırmaları (onScroll), sayfa yüklenmesi (onLoad) ve arka planda veri çekme (Fetch / AJAX) işlemleri.

  3. Rendering (Ekranı Çizme) İşlemleri: Tarayıcının kullanıcıya saniyede 60 kare (60 FPS) akıcı bir görüntü sunabilmek için HTML/CSS'i ekrana çizmesi.

İşte tüm bu süreçlerden Event Loop sorumludur. Tarayıcıdaki Event Loop, bir yandan asenkron kodları çalıştırırken bir yandan da ekranın donmaması için Rendering (Çizim) işlemlerine zaman ayırmak zorundadır.

Ancak Event Loop ve kuyruk detaylarına geçmeden önce, kodumuzun çalışırken bilgisayar belleğini (RAM) nasıl kullandığını anlamamız gerekir.

2. Bellek Kullanımı: Stack (Yığın) vs. Heap (Öbek)

İşlemci (CPU), aritmetik ve mantıksal işlemleri yaparken kendi içindeki ultra hızlı Register (Yazmaç) hücrelerini kullanır. Ancak bu alanlar baytlar seviyesindedir ve tüm program verisini tutmak için imkânsız derecede küçüktür. Bu yüzden işletim sistemi, programımız çalıştığında sistem belleğinden (RAM) bize izole bir alan ayırır.

JavaScript işletim motorları (Google V8, SpiderMonkey vb.), belleği temel olarak iki farklı alana ayırarak yönetir: Stack (Yığın) ve Heap (Öbek).

Aralarındaki temel ayrım "Hız vs. Esneklik" dengesidir.

Stack vs Heap in Swift: What Every iOS Developer Should Actually Know | by  Mobile App Developer | Stackademic

2.1 Stack Belleği (Statik Bellek)

Stack, boyutu ve sınırları önceden bilinen verilerin saklandığı, bilgisayar bilimlerindeki LIFO (Last In, First Out - Son Giren İlk Çıkar) mantığıyla çalışan bellek alanıdır.

  • Erişim Hızı: Işık hızındadır. CPU, verinin bellekte tam olarak nerede durduğunu ve kaç bayt kapladığını milimetrik olarak bilir.

  • Neler Saklanır?

    • İlkel (Primitive) veri tipleri (number, string, boolean, undefined, null, symbol, bigint).

    • Fonksiyon çağrılarının adresleri ve yerel değişken isimleri.

  • Çalışma Mantığı: Üst üste konulmuş yemek tabakları gibidir. Yeni bir ilkel değişken tanımlandığında Stack'in en üstüne eklenir, işi bittiğinde en üstten kaldırılır (push / pop).

let age = 25; 
// age değişkeninin kendisi ve "25" değeri DOĞRUDAN Stack alanındaki tek bir hücreye yazılır.

2.2 Heap Belleği (Dinamik/Sanal Bellek)

Heap, boyutları önceden kestirilemeyen, dinamik olarak büyüyüp küçülebilen devasa bir bellek alanıdır. İşletim sistemindeki Sanal Bellek (Virtual Memory) ve RAM'in esnek alanları burada kullanılır.

  • Erişim Hızı: Stack belleğe göre daha yavaştır. Bellek blokları rastgele ve dağınıktır; veriler bellek adresleri (referanslar) üzerinden aranıp bulunur.

  • Neler Saklanır?

    • Karmaşık (Reference/Non-Primitive) veri tipleri (Objects, Arrays, Functions).

  • Çalışma Mantığı: Bir nesne (object) oluşturduğunuzda, nesnenin verisi Heap üzerinde rastgele boş bir alana yazılır. Stack alanında ise yalnızca bu verinin Heap'teki adresini gösteren küçük bir Referans (Pointer) saklanır.

let user = { name: "Ahmet", age: 25 };
// "user" ismi Stack'tedir ama değeri bir adrestir (#0x0014A).
// Gerçek { name: "Ahmet", age: 25 } verisi ise Heap'te o adreste yaşar.

2.3 Kod Üzerinden Bellek Analizi ve Pratik Sonuçlar

Bu mimariyi anladığımızda, JavaScript'teki bazı "garip" davranışların arkasındaki sebepleri de çözeriz:

A. Değer (Value) vs. Referans (Reference) Kopyalama

// İlkel Tip (Stack)
let a = 10;
let b = a; // b'ye a'nın DEĞERİ kopyalanır.
b = 20;
console.log(a); // 10 (a etkilenmez, çünkü Stack'te bağımsız hücrelerdedirler).

// Referans Tip (Heap)
let obj1 = { name: "Ali" };
let obj2 = obj1; // obj2'ye nesnenin kendisi DEĞİL, Stack'teki ADRESİ kopyalanır!

obj2.name = "Veli";
console.log(obj1.name); // "Veli" ! (Çünkü ikisi de Heap'teki aynı adrese bakıyor)

B. const İle Tanımlanan Nesnelerin İçeriği Nasıl Değişebilir?

Yazılımcıların sıkça kafasını karıştıran durumlardan biri şudur:

const car = { brand: "BMW" };
car.brand = "Audi"; // HATA VERMEZ!

Neden Hata Vermez?

const anahtar kelimesi Stack'te duran değeri korur. car değişkeninin Stack'teki değeri bir bellek adresidir (#0x008F). Siz car.brand = "Audi" dediğinizde Heap'teki nesnenin içini değiştirirsiniz. Stack'te duran #0x008F adresi değişmediği için JavaScript motoru hiçbir kuralı ihlal edilmemiş sayar!

Ancak adresi değiştirmeye kalkarsanız hata alırsınız:

car = { brand: "Mercedes" }; // 🔴 Uncaught TypeError: Assignment to constant variable.
// Çünkü yeni bir nesne oluşturup Stack'teki adresi değiştirmeye çalıştınız!

3. Call Stack (Çağrı Yığını) Nasıl Çalışır ve Nasıl Taşar (Stack Overflow)?

JavaScript motorlarının (V8, SpiderMonkey vb.) hafızadaki Stack (Yığın) alanını donanımsal verileri tutmak için kullandığını gördük. Peki bu Stack alanının üzerinde kodlarımız, fonksiyonlarımız tam olarak nasıl sırayla çalıştırılır?

İşte bu süreci yöneten mekanizmaya Call Stack (Çağrı Yığını) denir.

Call Stack, JavaScript motorunun o an kodun neresinde olduğunu, hangi fonksiyonun çalıştığını ve o fonksiyon bitince sıradaki hangi fonksiyona dönülmesi gerektiğini takip ettiği LIFO (Last In, First Out - Son Giren İlk Çıkar) esaslı bir veri yapısıdır.

💡Hatırlatma: JavaScript doğası gereği Single-Thread (Tek İş Parçacıklı) bir dildir. Bu nedenle tarayıcıda yalnızca TEK BİR Call Stack bulunur. JavaScript aynı salisede iki farklı fonksiyonu birden çalıştıramaz; işleri sırayla, üst üste koyarak ve en üsttekini bitirerek yapmak zorundadır.

Fonksiyonlar Ne Zaman ve Nasıl Stack Belleğe Gider?

  • Sayfanın İlk Yüklenme Anı (Initial Load): JavaScript dosyası tarayıcı tarafından okunduğunda, tüm koda yön veren Global Execution Context (main()) Stack'in en altına bir temel olarak yerleşir. Senkron kodlar ve başlangıç fonksiyonları sırayla çalışıp bittiğinde Call Stack tamamen boşalır.

  • Kullanıcı Etkileşimleri ve Olaylar (Event Triggers): Sayfa yüklendikten sonra Call Stack boşta bekler. Kullanıcı sayfadaki bir butona tıkladığında (onClick), fareyi kaydırdığında veya klavyeden bir tuşa bastığında; tarayıcı bu olayı yakalar ve olaya bağlı fonksiyonu anında boşta bekleyen Call Stack'in içine fırlatır (PUSH). Fonksiyon milisaniyeler içinde çalışıp biter (POP) ve Call Stack bir sonraki etkileşime kadar tekrar bomboş kalır.

Kaldığı Yere Dönme Mantığı: Return Address (Geri Dönüş Adresi)

Bir fonksiyon çalıştığında, Call Stack'e sadece fonksiyonun ismi ve değişkenleri yüklenmez. JavaScript motoru, oluşturduğu Execution Context (Yürütme Bağlamı) içerisine Return Address (Geri Dönüş Adresi) adında kritik bir bilgi ekler.

Call Stack Yürütme Kod Örneği

Bu mekanizmayı kod üzerinde inceleyelim:

Adım Adım Çalışma Trafiği:

  1. Kullanıcı tetiklemesiyle printSquare(4) Stack'e girer.

  2. square(4) çağrılır ve Stack'e printSquare'in üzerine eklenir.

  3. multiply(4, 4) çağrılır ve Stack'in en üstüne yerleşir. (Stack şu an en dolu halindedir).

  4. Boşalma ve Dönüş Aşaması (POP Sıralaması):

    • En üstteki multiply(4, 4) çalışır, 16 sonucunu döner ve bellekten silinir (POP).

    • Kontrol kaydedilen dönüş adresi sayesinde square fonksiyonunun kaldığı satıra döner. O da 16 sonucunu alır, işini tamamlar ve silinir (POP).

    • Kontrol printSquare fonksiyonuna geçer, kaldığı satırdan devam ederek console.log(16) satırını çalıştırır ve bellekten silinir (POP).

    • Call Stack bir sonraki kullanıcı tıklamasına kadar tekrar bomboş bir hale gelir.

Call Stack Neden Tıkanır ve Taşar?

1. Stack Overflow (Yığın Taşması)

RAM üzerindeki Stack alanı sınırlı ve statik bir boyuta sahiptir. Eğer fonksiyonlar bitip Call Stack'ten silinmeden (POP edilmeden) sürekli yenileri yığına eklenirse (PUSH), yığın alanı sınırını aşar.

function recursiveFunction() {
    recursiveFunction(); // Durma koşulu yok, dönüş adresi alıp sürekli kendini PUSH ediyor!
}

recursiveFunction();
// 🔴 Uncaught RangeError: Maximum call stack size exceeded

2. Main Thread'in Bloklanması (Unresponsive UI)

Call Stack'in en kritik kuralı aynı anda sadece en üstteki fonksiyonu çalıştırabilmesidir.

Eğer Call Stack'e giren bir fonksiyon (örneğin 5 saniye süren karmaşık bir senkron döngü) yığına oturursa, kullanıcı o 5 saniye boyunca ekrandaki hiçbir butona tıklayamaz. Çünkü kullanıcının tıklama fonksiyonlarının Stack'e girebilmesi için önce en üstteki o ağır fonksiyonun POP edilip Stack'ten çıkması gerekir!

function heavySyncTask() {
    let start = Date.now();
    while (Date.now() - start < 5000) {
        // 5 saniye boyunca Call Stack'i işgal eden senkron döngü
    }
}

Sonuç ve Sıradaki Adım

Bu makalede, istemci tarafındaki (Client-Side) bellek mimarisini ve JavaScript’in kalbi olan Call Stack’in çalışma prensiplerini inceledik. Artık verilerimizin RAM üzerindeki Stack ve Heap bölgelerinde nasıl saklandığını, fonksiyon çağrılarının LIFO mantığıyla nasıl yürütüldüğünü ve geri dönüş adresleri sayesinde kodun nerede kaldığını nasıl bildiğini biliyoruz.

Ancak tek hatlı (Single-Thread) bu yapının doğurduğu kritik bir soru hâlâ ortada duruyor:

"Madem JavaScript aynı anda yalnızca tek bir fonksiyonu çalıştırabiliyor ve ağır işlerde Call Stack kilitleniyor; o halde setTimeout, Fetch/AJAX ağ istekleri veya kullanıcı etkileşimleri sayfayı dondurmadan arka planda nasıl yürütülebiliyor?"

Bu sorunun yanıtı, tarayıcının görünmez kahramanlarında saklı. Serimizin bir sonraki makalesinde Web APIs, Task Queue (Macrotask vs. Microtask) ve Event Loop mekanizmasını masaya yatırarak JavaScript’in asenkron gücünü keşfedeceğiz.