← Kursa Dön
📄 Text · 30 min

Performans Optimizasyonu

Hız Neden Önemli?

Google'ın araştırmasına göre, bir web sayfası 3 saniyeden uzun sürerse kullanıcıların %53'ü sayfayı terk eder. Amazon ise her 100ms gecikmenin satışlarda %1 düşüşe yol açtığını keşfetmiş. Performans sadece teknik bir metrik değil — doğrudan kullanıcı deneyimi ve gelire etki eder.

Bu derste JavaScript performansının altı kritik alanını keşfedeceğiz: memory leak'ler (bellek sızıntıları), garbage collection'ın nasıl çalıştığı, debounce/throttle ile event optimizasyonu, lazy loading ile sayfa hızlandırma, Web Vitals metrikleri ve Chrome DevTools ile profiling. Her biri gerçek dünya senaryolarıyla, ölçülebilir sonuçlarla.


Memory Leaks: Bellek Sızıntıları

Bellek sızıntısı, artık kullanılmayan ama temizlenemeyen bellektir. Bir oda düşün: misafir gitti ama tabaklar, bardaklar masada kaldı. Bir misafir için sorun değil ama 1000 misafirden sonra oda çöplüğe döner. Aynı şekilde, leak'li bir web uygulaması saatlerce açık kaldığında yavaşlar, donar, çöker.

Yaygın Memory Leak Kaynakları

1. Temizlenmeyen Event Listener'lar

// ❌ Leak: Her çağrıda yeni listener ekleniyor, eskisi kalmıyor
function setupTracker() {
  window.addEventListener("scroll", () => {
    trackScrollPosition();
  });
  // Fonksiyon her çağrıldığında yeni listener birikir!
}

// ✅ Düzeltme: Listener'ı sakla ve temizle
function setupTracker() {
  const handler = () => trackScrollPosition();
  window.addEventListener("scroll", handler);

  // Cleanup fonksiyonu döndür
  return () => {
    window.removeEventListener("scroll", handler);
  };
}

// React, Vue gibi framework'lerde
// Component unmount olduğunda cleanup çağrılmalı
const cleanup = setupTracker();
// ... component yaşamını sürdürür ...
cleanup(); // Component kaldırılınca

2. Temizlenmeyen Timer'lar

// ❌ Leak: setInterval hiç durdurulmuyor
function startPolling() {
  setInterval(async () => {
    const data = await fetch("/api/status");
    updateUI(data);
  }, 5000);
  // Sayfa değişse bile interval çalışmaya devam eder
}

// ✅ Düzeltme: Timer ID'sini sakla ve temizle
function startPolling() {
  const intervalId = setInterval(async () => {
    const data = await fetch("/api/status");
    updateUI(data);
  }, 5000);

  return () => clearInterval(intervalId);
}

const stopPolling = startPolling();
// Gerektiğinde:
stopPolling();

3. DOM Referansları

// ❌ Leak: Element DOM'dan silindi ama JavaScript referansı duruyor
const elements = [];

function addItem(text) {
  const el = document.createElement("div");
  el.textContent = text;
  document.body.appendChild(el);
  elements.push(el); // Referans tutuluyor
}

function removeAllItems() {
  document.body.innerHTML = "";
  // elements dizisi hâlâ eski DOM elemanlarına referans tutuyor!
  // Garbage collector bunları temizleyemez
}

// ✅ Düzeltme: DOM referanslarını da temizle
function removeAllItems() {
  document.body.innerHTML = "";
  elements.length = 0; // Referansları temizle
}

4. Closure'larda Tutulan Büyük Veriler

// ❌ Leak: Closure büyük veriyi sonsuza dek tutuyor
function processData() {
  const hugeArray = new Array(1_000_000).fill("data");

  // Bu event handler hugeArray'e closure üzerinden erişiyor
  // Handler kaldırılmadıkça hugeArray bellekte kalır
  document.getElementById("btn").addEventListener("click", () => {
    console.log(hugeArray.length);
  });
}

// ✅ Düzeltme: Sadece ihtiyacın olanı tut
function processData() {
  const hugeArray = new Array(1_000_000).fill("data");
  const length = hugeArray.length; // Sadece sonucu sakla

  document.getElementById("btn").addEventListener("click", () => {
    console.log(length); // Büyük array'e referans yok
  });
  // hugeArray artık garbage collect edilebilir
}

Memory Leak Tespit Etme

// Chrome DevTools kullan:
// 1. F12 → Memory tab → "Take heap snapshot"
// 2. Uygulamayı kullan (sayfalar arası geçiş, modal aç/kapa)
// 3. Yeni snapshot al
// 4. İki snapshot'ı karşılaştır — büyüyen nesneleri bul

// Programmatic kontrol
console.log("Bellek:", performance.memory?.usedJSHeapSize / 1024 / 1024, "MB");
// Not: performance.memory sadece Chrome'da var

Garbage Collection: Otomatik Temizlik

JavaScript'te bellek yönetimi otomatiktirmalloc/free gibi manuel yönetim yok. Garbage Collector (GC), artık erişilemeyen nesneleri otomatik temizler. Ama GC'nin nasıl çalıştığını bilmek, memory leak'leri anlamak ve önlemek için önemli.

Erişilebilirlik (Reachability)

GC, köklerden (roots) erişilebilen tüm nesneleri canlı sayar. Kökler:

  • Global değişkenler (window, globalThis)

  • Çalışan fonksiyonların local değişkenleri

  • Event listener callback'leri

// Erişilebilir — GC temizleyemez
let user = { name: "Ali" }; // user → { name: "Ali" } (kökten erişilebilir)

// Erişilemez — GC temizleyebilir
user = null; // { name: "Ali" } artık hiçbir yerden erişilemez → GC toplar

// Döngüsel referans — modern GC'ler bunu da toplar
function example() {
  const a = {};
  const b = {};
  a.ref = b;
  b.ref = a;
  // Fonksiyon bittiğinde a ve b'ye kökten erişilemez
  // Modern GC (mark-and-sweep) döngüsel referansları da toplar
}

WeakRef ve FinalizationRegistry

// WeakRef — zayıf referans, GC'yi engellemez
let largeObject = { data: new Array(1_000_000) };
const weakRef = new WeakRef(largeObject);

// Nesneye erişim
console.log(weakRef.deref()?.data.length); // 1000000

// Orijinal referansı kaldır
largeObject = null;
// GC çalıştığında nesne temizlenebilir
// weakRef.deref() → undefined döner

// FinalizationRegistry — nesne temizlendiğinde bildirim
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`${heldValue} temizlendi`);
});

let obj = { name: "test" };
registry.register(obj, "test nesnesi"); // GC temizlediğinde bildirir
obj = null;

⚠️ Dikkat: WeakRef ve FinalizationRegistry ileri seviye araçlardır. Günlük kodda nadiren gerekir. Cache implementasyonları ve kaynak yönetimi dışında kullanımından kaçın.


Debounce ve Throttle: Event Fırtınasını Dindirmek

Kullanıcı yazı yazarken her tuşa basışta API çağrısı mı yapacaksın? Ya da scroll ederken her piksel'de hesaplama mı? Bu hem performansı öldürür hem de gereksiz sunucu yükü oluşturur. İşte debounce ve throttle bu sorunu çözer.

Debounce: "Dur, Bitir, Sonra Yap"

Debounce, olayın durmasını bekler. Asansör kapısı gibi düşün: biri geldiğinde kapı hemen kapanmaz, belirli bir süre kimse gelmezse kapanır. Biri daha gelirse süre sıfırlanır.

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timeoutId: ReturnType<typeof setTimeout>;

  return function (...args: Parameters<T>) {
    clearTimeout(timeoutId); // Önceki zamanlayıcıyı iptal et
    timeoutId = setTimeout(() => {
      fn(...args); // Delay sonra çalıştır
    }, delay);
  };
}

// Kullanım: Arama kutusu
const searchInput = document.getElementById("search");

const debouncedSearch = debounce(async (query: string) => {
  console.log(`API çağrısı: "${query}"`);
  const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
  renderResults(results);
}, 300);

searchInput.addEventListener("input", (e) => {
  debouncedSearch((e.target as HTMLInputElement).value);
});

// Kullanıcı "javascript" yazarken:
// j → a → v → a → s → c → r → i → p → t
// Sadece SON harf yazıldıktan 300ms sonra TEK BİR API çağrısı yapılır
// Debounce olmasaydı: 10 API çağrısı!

Throttle: "En Fazla Şu Sıklıkta Yap"

Throttle, olayı belirli aralıklarla sınırlar. Hız sınırı tabelası gibi: ne kadar hızlı gitmek istersen iste, en fazla 120 km/h.

function throttle<T extends (...args: any[]) => any>(
  fn: T,
  interval: number
): (...args: Parameters<T>) => void {
  let lastTime = 0;
  let timeoutId: ReturnType<typeof setTimeout> | null = null;

  return function (...args: Parameters<T>) {
    const now = Date.now();

    if (now - lastTime >= interval) {
      lastTime = now;
      fn(...args);
    } else if (!timeoutId) {
      // Son çağrıyı da yakala
      timeoutId = setTimeout(() => {
        lastTime = Date.now();
        timeoutId = null;
        fn(...args);
      }, interval - (now - lastTime));
    }
  };
}

// Kullanım: Scroll pozisyonu izleme
const throttledScroll = throttle(() => {
  const scrollPercent = (window.scrollY / document.body.scrollHeight) * 100;
  updateProgressBar(scrollPercent);
}, 100); // En fazla her 100ms'de bir

window.addEventListener("scroll", throttledScroll);

// Kullanıcı 1 saniye boyunca scroll ederse:
// Throttle olmadan: ~60 hesaplama (her frame'de)
// Throttle ile: ~10 hesaplama (her 100ms'de bir)

Ne Zaman Hangisi?

Debounce → Kullanıcı DURDUĞUNDA çalıştır
├── Arama kutusu (typing bitince ara)
├── Form validasyonu (yazmayı bitirince kontrol et)
├── Pencere boyutlandırma (boyutlandırma bitince layout hesapla)
└── Auto-save (yazmayı durdurunca kaydet)

Throttle → Belirli ARALIKTA çalıştır
├── Scroll event (her 100ms'de pozisyon güncelle)
├── Mouse move (her 50ms'de koordinat al)
├── API rate limiting (saniyede max 10 istek)
└── Oyun input'u (her frame'de değil, belirli aralıkta)

Lazy Loading: İhtiyaç Olunca Yükle

Lazy loading, kaynakları (JavaScript, görseller, bileşenler) sayfa ilk yüklendiğinde değil, ihtiyaç duyulduğunda yükler. Bir kütüphanedeki tüm kitapları evine taşımak yerine, okuyacağın kitabı o an ödünç almak gibi.

Dinamik Import

// ❌ Statik import — her zaman yüklenir (bundle'a dahil)
import { Chart } from "chart.js"; // 200KB!
import { PDFExporter } from "./pdf-exporter"; // 150KB

// Kullanıcı hiç grafik görüntülemese veya PDF indirmese bile yüklendi

// ✅ Dinamik import — sadece gerektiğinde yükle
async function showChart(data) {
  const { Chart } = await import("chart.js"); // Tıklandığında yüklenir
  const chart = new Chart(canvas, {
    type: "bar",
    data,
  });
}

async function downloadPDF(report) {
  const { PDFExporter } = await import("./pdf-exporter");
  const exporter = new PDFExporter();
  return exporter.generate(report);
}

// Buton tıklandığında yükle
document.getElementById("chart-btn").addEventListener("click", () => {
  showChart(chartData); // İlk tıklamada 200KB indirilir
});

Route-Based Code Splitting

// React Router ile lazy loading
import { lazy, Suspense } from "react";

// Her sayfa ayrı chunk olarak yüklenir
const HomePage = lazy(() => import("./pages/HomePage"));
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const AdminPanel = lazy(() => import("./pages/AdminPanel")); // Admin olmayanlar hiç yüklemez

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

Görsel Lazy Loading

<!-- Native lazy loading (tarayıcı desteği geniş) -->
<img src="photo.jpg" loading="lazy" alt="Fotoğraf" />

<!-- Intersection Observer ile (daha fazla kontrol) -->
<img data-src="photo.jpg" class="lazy" alt="Fotoğraf" />
// IntersectionObserver ile lazy loading
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.classList.remove("lazy");
      observer.unobserve(img);
    }
  });
}, { rootMargin: "200px" }); // 200px erken yükle

document.querySelectorAll("img.lazy").forEach(img => observer.observe(img));

Web Vitals: Google'ın Performans Metrikleri

Web Vitals, Google'ın web performansı için belirlediği standart metrikler. Core Web Vitals üç tanedir ve SEO sıralamasını doğrudan etkiler:

1. LCP (Largest Contentful Paint) — En Büyük İçerik Boyama

Sayfadaki en büyük elementin (genelde hero image veya başlık) ne zaman görünür olduğu.

✅ İyi:     ≤ 2.5 saniye
⚠️ İyileştir: 2.5 - 4.0 saniye
❌ Kötü:    > 4.0 saniye

İyileştirme:

  • Görselleri optimize et (WebP, AVIF formatı)

  • Critical CSS'i inline yap

  • Font display: swap kullan

  • Server response time'ı düşür

2. INP (Interaction to Next Paint) — Etkileşim Tepkisi

Kullanıcı tıkladığında/yazdığında sayfanın ne kadar sürede tepki verdiği.

✅ İyi:     ≤ 200ms
⚠️ İyileştir: 200 - 500ms
❌ Kötü:    > 500ms

İyileştirme:

  • Ağır hesaplamaları Web Worker'a taşı

  • Long task'ları parçala

  • JavaScript bundle boyutunu küçült

3. CLS (Cumulative Layout Shift) — Kümülatif Düzen Kayması

Sayfanın yüklenirken ne kadar "zıpladığı" — elementlerin beklenmedik şekilde yer değiştirmesi.

✅ İyi:     ≤ 0.1
⚠️ İyileştir: 0.1 - 0.25
❌ Kötü:    > 0.25

İyileştirme:

  • Görsellere width/height belirle

  • Dinamik içerik için yer ayır (skeleton)

  • Fontları preload et

Web Vitals Ölçümü

// web-vitals kütüphanesi ile
import { onLCP, onINP, onCLS } from "web-vitals";

onLCP((metric) => {
  console.log("LCP:", metric.value, "ms");
  analytics.send("web-vitals", {
    name: "LCP",
    value: metric.value,
    rating: metric.rating, // "good" | "needs-improvement" | "poor"
  });
});

onINP((metric) => {
  console.log("INP:", metric.value, "ms");
});

onCLS((metric) => {
  console.log("CLS:", metric.value);
});

Profiling: Chrome DevTools ile Performans Analizi

Performance Tab

Chrome DevTools → Performance tab:

1. "Record" butonuna bas
2. Uygulamayı kullan (scroll, tıkla, navigate et)
3. "Stop" butonuna bas
4. Sonuçları analiz et:

├── Summary: CPU kullanımı dağılımı
│   ├── Scripting (JavaScript) — sarı
│   ├── Rendering (layout/paint) — mor
│   ├── Painting — yeşil
│   └── Idle — boş
│
├── Main thread: Her görevin süresi
│   └── Long tasks (50ms+) kırmızı işaretli — BUNLARI DÜZELT
│
└── Network: Kaynak yükleme süreleri

Programmatic Performance Measurement

// Performance API ile ölçüm
function measurePerformance(name, fn) {
  performance.mark(`${name}-start`);

  const result = fn();

  performance.mark(`${name}-end`);
  performance.measure(name, `${name}-start`, `${name}-end`);

  const measure = performance.getEntriesByName(name)[0];
  console.log(`${name}: ${measure.duration.toFixed(2)}ms`);

  return result;
}

// Kullanım
const sortedData = measurePerformance("sort-users", () => {
  return users.sort((a, b) => a.name.localeCompare(b.name));
});
// "sort-users: 12.34ms"

// Async ölçüm
async function measureAsync(name, fn) {
  const start = performance.now();
  const result = await fn();
  const duration = performance.now() - start;
  console.log(`${name}: ${duration.toFixed(2)}ms`);
  return result;
}

await measureAsync("fetch-users", () =>
  fetch("/api/users").then(r => r.json())
);

Genel Optimizasyon Stratejileri

// 1. Gereksiz render/hesaplamayı önle — memoization
function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  // Ağır hesaplama
  let result = 0;
  for (let i = 0; i < n; i++) result += Math.sqrt(i);
  return result;
});

expensiveCalc(1000000); // İlk çağrı: yavaş
expensiveCalc(1000000); // İkinci çağrı: anında (cache'den)

// 2. requestIdleCallback — boş zamanda iş yap
function processInBackground(tasks) {
  function process(deadline) {
    while (tasks.length > 0 && deadline.timeRemaining() > 5) {
      const task = tasks.shift();
      task(); // Tarayıcı boştayken çalıştır
    }

    if (tasks.length > 0) {
      requestIdleCallback(process);
    }
  }

  requestIdleCallback(process);
}

// 3. Virtual scrolling fikri — binlerce öğe göstermek
// Sadece görünen öğeleri DOM'da tut
// 10.000 satırlık liste → ekranda sadece 20 satır render et
// Kaydırdıkça güncelle

Özet

  • Memory Leaks: Temizlenmeyen event listener, timer, DOM referansı, closure. Cleanup fonksiyonlarını her zaman çağır.

  • Garbage Collection: Mark-and-sweep — erişilemeyen nesneler toplanır. WeakRef/WeakMap ile GC'yi engelleme.

  • Debounce: Kullanıcı durduğunda çalıştır (arama, auto-save). Throttle: Belirli aralıkta çalıştır (scroll, mouse).

  • Lazy Loading: Dinamik import ile code splitting, route-based lazy loading, görsel lazy loading.

  • Web Vitals: LCP (≤2.5s), INP (≤200ms), CLS (≤0.1) — Google SEO ve kullanıcı deneyimi metrikleri.

  • Profiling: Chrome DevTools Performance tab, performance.mark/measure API, long task tespiti.

  • Performans optimizasyonu ölçerek yap — tahmin etme, ölç.