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ınca2. 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 varGarbage Collection: Otomatik Temizlik
JavaScript'te bellek yönetimi otomatiktir — malloc/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:
WeakRefveFinalizationRegistryileri 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üreleriProgrammatic 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/measureAPI, long task tespiti.Performans optimizasyonu ölçerek yap — tahmin etme, ölç.
AI Asistan
Sorularını yanıtlamaya hazır