← Blog · 20 Haziran 2026

Three.js sahnesi SPA'da bellek sızdırır: dispose disiplini ve paylaşılan kaynak tuzağı

Bu sitenin anasayfasında gerçek bir 3D oda var: masa, klavyeye yüklenen bir karakter, halıda bir kedi. Blog’a geçince o sahne sökülüyor. İlk yazdığımda söküm “çalışıyordu” — ama her home → blog → home turunda Chrome’un bellek grafiği biraz daha yukarı tırmanıyordu. Sebep, Three.js’in en sinsi tarafı: sahneden çıkarmak, GPU’dan çıkarmak değildir.

scene.remove yetmiyor

scene.remove(obj) objeyi sahne grafiğinden alır, ama geometry, material ve texture hâlâ GPU belleğinde (VRAM) durur. GLB’lerin texture’ları MB’larca yer kaplar; her ziyarette yenisi yüklenip eskisi bırakılmazsa VRAM birikir de birikir. Tarayıcı bir noktada sekmeyi yavaşlatır ya da WebGL context’ini büsbütün kaybeder.

Üç kaynağı da elle bırakmak gerekiyor:

export function disposeObject(root: Object3D) {
  root.traverse((child) => {
    const mesh = child as Mesh;
    if (!mesh.isMesh) return;
    mesh.geometry?.dispose();
    const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
    for (const mat of materials) {
      if (mat) disposeMaterial(mat);
    }
  });
}

Asıl tuzak: paylaşılan kaynağı dispose etme

İlk versiyonum her texture’ı gözü kapalı dispose ediyordu. Sonuç ters tepti: sahneye geri döndüğümde modeller bir an gri ve bozuk geliyordu. Sebep, matcap texture’larım modül-seviyesi singleton’dı — bir kez yaratılıp tüm sahnede paylaşılıyordu. Onları dispose edince, home → blog → home dönüşünde aynı Texture objesi GPU’ya tekrar upload edilmek zorunda kalıyordu (churn), üstelik global state kırılganlaşıyordu.

Çözüm: sahibi olmadığınız kaynağı bırakmayın. Paylaşılan texture’lar için bir atlama listesi tutuyorum:

const sharedTextures = new Set<Texture>([...Object.values(matcaps), blobTexture]);

export function disposeMaterial(mat: Material) {
  for (const value of Object.values(mat)) {
    const tex = value as Texture;
    if (tex && tex.isTexture && !sharedTextures.has(tex)) tex.dispose();
  }
  mat.dispose();
}

Kural basit ama kolay kaçar: bir kaynağı yalnızca onu yaratan dispose etmeli. Per-obje yüklenen GLB texture’ları → bırak. Tüm sahnenin paylaştığı singleton → dokunma.

Söküm tek yerde

Bütün bunlar route değişiminde tetikleniyor. Vue tarafında onBeforeUnmount her şeyi sırayla söküyor:

onBeforeUnmount(() => {
  destroyMotion();    // GSAP/scroll bağlarını kopar
  destroyWorld();     // proceduralObjects → disposeObject
  destroyRenderer();  // renderer.dispose()
});

Bağlandığınız her şeyin — animasyon döngüsü, scroll listener’ları, renderer — aynı yerde ve aynı disiplinle sökülmesi gerekiyor. Bir 3D sahneyi SPA içinde tutmanın bedeli bu; ama bedeli bir kez doğru ödersen, sahne bütün gün açılıp kapanır ve bellek düz çizgi kalır.

three.js’in obje dispose rehberi bunu açıkça söylüyor: geometry, material ve texture’lar elle bırakılmalı, çünkü three bunların yaşam döngüsünü sizin için yönetmez.