React'te Performans Optimizasyonu: memo, useMemo ve useCallback Rehberi
React'te Performans Optimizasyonu: memo, useMemo ve useCallback Rehberi
React, bileşen tabanlı mimarisi ve sanal DOM yapısıyla zaten performanslı bir kütüphanedir. Ancak uygulamanız büyüdükçe, gereksiz render'lar ve pahalı hesaplamalar ciddi performans sorunlarına yol açabilir. İşte tam bu noktada React.memo, useMemo ve useCallback devreye girer.
Bu yazıda, bu üç optimizasyon aracını derinlemesine inceleyecek, gerçek dünya senaryolarıyla ne zaman kullanılması (ve ne zaman kullanılmaması) gerektiğini öğreneceksiniz.
React'te Gereksiz Render Problemi Nedir?
React'te bir bileşenin yeniden render olmasının üç temel nedeni vardır:
- State değişikliği — Bileşenin kendi state'i güncellendiğinde
- Props değişikliği — Üst bileşenden gelen props değiştiğinde
- Üst bileşen render olduğunda — Parent bileşen her render olduğunda, child bileşenler de varsayılan olarak yeniden render olur
Üçüncü madde, performans sorunlarının en yaygın kaynağıdır. Üst bileşen render olduğunda, child bileşenin props'ları değişmemiş olsa bile yeniden render edilir. Küçük uygulamalarda bu fark edilmez, ancak yüzlerce bileşenden oluşan bir listede veya karmaşık bir dashboard'da ciddi gecikmelere neden olabilir.
// ❌ Problem: ParentComponent her render olduğunda
// ExpensiveChild da gereksiz yere yeniden render olur
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Sayaç: {count}
</button>
{/* count değiştiğinde bu bileşen de render olur,
ancak hiçbir prop'u değişmedi */}
<ExpensiveChild title="Sabit Başlık" />
</div>
);
}1. React.memo: Bileşen Seviyesinde Memoization
React.memo, bir bileşeni higher-order component (HOC) olarak sarmalayarak, props'ları değişmediği sürece yeniden render edilmesini engeller. Yüzeysel (shallow) karşılaştırma yapar.
Temel Kullanım
import { memo } from 'react';
const ExpensiveChild = memo(function ExpensiveChild({ title, data }) {
console.log('ExpensiveChild render oldu!');
return (
<div>
<h2>{title}</h2>
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
});Artık ExpensiveChild, yalnızca title veya data props'u gerçekten değiştiğinde render olacaktır.
Özel Karşılaştırma Fonksiyonu
Varsayılan shallow comparison yeterli olmadığında, kendi karşılaştırma fonksiyonunuzu yazabilirsiniz:
const UserCard = memo(
function UserCard({ user, onSelect }) {
console.log(`UserCard render: ${user.name}`);
return (
<div onClick={() => onSelect(user.id)}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
},
(prevProps, nextProps) => {
// true dönerse render YAPILMAZ, false dönerse render YAPILIR
return prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email;
}
);React.memo Ne Zaman Kullanılmalı?
| ✅ Kullanın | ❌ Kullanmayın |
|---|---|
| Sık render olan parent altındaki child bileşenlerde | Props'ları sürekli değişen bileşenlerde |
| Render maliyeti yüksek bileşenlerde | Basit ve hafif bileşenlerde |
| Aynı props ile aynı çıktıyı üreten saf (pure) bileşenlerde | Memo'nun karşılaştırma maliyeti render maliyetinden yüksekse |
2. useMemo: Hesaplanmış Değerleri Önbellekleme
useMemo, pahalı bir hesaplamanın sonucunu önbelleğe alır ve yalnızca bağımlılıklar değiştiğinde yeniden hesaplar. Gereksiz hesaplamaları önlemek için kullanılır.
Sözdizimi
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);Gerçek Dünya Örneği: Filtreleme ve Sıralama
import { useState, useMemo } from 'react';
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('name');
const [theme, setTheme] = useState('light');
// ✅ Sadece products, searchTerm veya sortBy değiştiğinde yeniden hesaplanır
const filteredAndSortedProducts = useMemo(() => {
console.log('Filtreleme ve sıralama çalışıyor...');
const filtered = products.filter(product =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
return 0;
});
}, [products, searchTerm, sortBy]);
// theme değiştiğinde filtreleme/sıralama tekrar çalışmaz ✅
return (
<div className={theme}>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Ürün ara..."
/>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">İsme Göre</option>
<option value="price">Fiyata Göre</option>
</select>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Tema Değiştir
</button>
<ul>
{filteredAndSortedProducts.map(product => (
<li key={product.id}>
{product.name} - {product.price} TL
</li>
))}
</ul>
</div>
);
}useMemo ile Referans Kararlılığı Sağlama
useMemo'nun göz ardı edilen bir kullanımı da nesne veya dizi referanslarını sabit tutmaktır. Bu, React.memo ile sarmalanmış child bileşenlere props geçerken kritik önem taşır:
function Dashboard({ userId }) {
const [refreshCount, setRefreshCount] = useState(0);
// ❌ Her render'da yeni bir nesne oluşturulur
// const config = { userId, theme: 'dark', language: 'tr' };
// ✅ Sadece userId değiştiğinde yeni nesne oluşturulur
const config = useMemo(() => ({
userId,
theme: 'dark',
language: 'tr'
}), [userId]);
return (
<div>
<button onClick={() => setRefreshCount(c => c + 1)}>
Yenile ({refreshCount})
</button>
{/* MemoizedWidget, config referansı değişmediği için
refreshCount değiştiğinde render olmaz */}
<MemoizedWidget config={config} />
</div>
);
}
const MemoizedWidget = memo(function Widget({ config }) {
console.log('Widget render oldu');
return <div>Kullanıcı: {config.userId}</div>;
});3. useCallback: Fonksiyon Referanslarını Önbellekleme
useCallback, bir fonksiyonun memoize edilmiş versiyonunu döndürür. Özünde useMemo'nun fonksiyonlar için özelleşmiş halidir. Child bileşenlere geçirilen callback fonksiyonlarının gereksiz yere yeniden oluşturulmasını engeller.
Sözdizimi
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);Not:
useCallback(fn, deps)ifadesiuseMemo(() => fn, deps)ile eşdeğerdir.
Gerçek Dünya Örneği: Liste ve Event Handler'lar
import { useState, useCallback, memo } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
// ✅ Fonksiyon referansı sabit kalır
const addTodo = useCallback(() => {
if (inputValue.trim()) {
setTodos(prev => [...prev, {
id: Date.now(),
text: inputValue,
completed: false
}]);
setInputValue('');
}
}, [inputValue]);
// ✅ Bağımlılık yok — fonksiyon asla yeniden oluşturulmaz
const toggleTodo = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
// ✅ Bağımlılık yok — fonksiyon asla yeniden oluşturulmaz
const deleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
<input
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="Yeni görev..."
/>
<button onClick={addTodo}>Ekle</button>
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
// memo ile sarmalandığı için, props değişmezse render olmaz
const TodoList = memo(function TodoList({ todos, onToggle, onDelete }) {
console.log('TodoList render oldu');
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
});
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log(`TodoItem render: ${todo.text}`);
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<span onClick={() => onToggle(todo.id)}>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Sil</button>
</li>
);
});Buradaki kilit nokta: toggleTodo ve deleteTodo fonksiyonları useCallback ile sarmalandığı ve fonksiyonel state güncellemesi (prev => ...) kullandığı için hiçbir bağımlılığa ihtiyaç duymaz. Bu sayede fonksiyon referansları hiç değişmez ve memo ile sarmalanmış child bileşenler gereksiz render'lardan korunur.
Üçünü Birlikte Kullanmak: Tam Bir Örnek
Gerçek bir uygulamada bu üç aracın birlikte nasıl çalıştığını görelim:
import { useState, useMemo, useCallback, memo } from 'react';
function EmployeeDashboard() {
const [employees, setEmployees] = useState(initialEmployees);
const [department, setDepartment] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [selectedId, setSelectedId] = useState(null);
// useMemo: Pahalı filtreleme işlemini önbelleğe al
const filteredEmployees = useMemo(() => {
console.log('Filtreleme hesaplanıyor...');
return employees
.filter(emp => department === 'all' || emp.department === department)
.filter(emp =>
emp.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [employees, department, searchQuery]);
// useMemo: İstatistikleri önbelleğe al
const stats = useMemo(() => ({
total: filteredEmployees.length,
avgSalary: filteredEmployees.length > 0
? filteredEmployees.reduce((sum, e) => sum + e.salary, 0) / filteredEmployees.length
: 0
}), [filteredEmployees]);
// useCallback: Event handler referansını sabit tut
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
const handleDelete = useCallback((id) => {
setEmployees(prev => prev.filter(emp => emp.id !== id));
}, []);
return (
<div>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<DepartmentFilter value={department} onChange={setDepartment} />
<StatsPanel stats={stats} />
<EmployeeList
employees={filteredEmployees}
selectedId={selectedId}
onSelect={handleSelect}
onDelete={handleDelete}
/>
</div>
);
}
// memo: Props değişmezse render olmasın
const StatsPanel = memo(function StatsPanel({ stats }) {
console.log('StatsPanel render');
return (
<div>
<span>Toplam: {stats.total}</span>
<span>Ort. Maaş: {stats.avgSalary.toFixed(0)} TL</span>
</div>
);
});
const EmployeeList = memo(function EmployeeList({
employees, selectedId, onSelect, onDelete
}) {
console.log('EmployeeList render');
return (
<ul>
{employees.map(emp => (
<EmployeeRow
key={emp.id}
employee={emp}
isSelected={emp.id === selectedId}
onSelect={onSelect}
onDelete={onDelete}
/>
))}
</ul>
);
});
const EmployeeRow = memo(function EmployeeRow({
employee, isSelected, onSelect, onDelete
}) {
console.log(`EmployeeRow render: ${employee.name}`);
return (
<li style={{ background: isSelected ? '#e0f0ff' : 'transparent' }}>
<span onClick={() => onSelect(employee.id)}>{employee.name}</span>
<span>{employee.department}</span>
<button onClick={() => onDelete(employee.id)}>Sil</button>
</li>
);
});Yaygın Hatalar ve Anti-Pattern'ler
1. Her Yerde Kullanmak
// ❌ Gereksiz — basit bir toplama işlemi memoize etmeye değmez
const total = useMemo(() => price + tax, [price, tax]);
// ✅ Doğrudan hesapla
const total = price + tax;2. useCallback'i memo Olmadan Kullanmak
// ❌ Child bileşen memo ile sarmalanmadıysa useCallback işe yaramaz
const handleClick = useCallback(() => {
console.log('Tıklandı');
}, []);
return <NormalChild onClick={handleClick} />;
// NormalChild zaten her seferinde render olacak!3. Bağımlılık Dizisini Yanlış Yönetmek
// ❌ Eksik bağımlılık — eski closure değerlerini kullanır (bug!)
const handleSubmit = useCallback(() => {
submitForm(formData);
}, []); // formData bağımlılık olarak eklenmeli
// ✅ Doğru kullanım
const handleSubmit = useCallback(() => {
submitForm(formData);
}, [formData]);Performans Ölçümü: React DevTools Profiler
Optimizasyonlarınızın gerçekten işe yarayıp yaramadığını ölçmek için React DevTools Profiler kullanın:
- Tarayıcıda React DevTools eklentisini açın
- Profiler sekmesine geçin
- Kayıt başlatıp uygulamada etkileşim yapın
- Her bileşenin render süresini ve sıklığını analiz edin
- "Highlight updates when components render" seçeneğini aktifleştirin
Altın kural: Önce ölçün, sonra optimize edin. Gerçek bir performans sorunu olmadan erken optimizasyon yapmak, kodunuzu gereksiz yere karmaşıklaştırır.
Hızlı Karşılaştırma Tablosu
| Özellik | React.memo |
useMemo |
useCallback |
|---|---|---|---|
| Ne yapar? | Bileşeni memoize eder | Değeri memoize eder | Fonksiyonu memoize eder |
| Türü | Higher-Order Component | Hook | Hook |
| Amacı | Gereksiz render'ı engeller | Pahalı hesaplamayı önler | Fonksiyon referansını sabitler |
| Kullanım yeri | Bileşen tanımı | Bileşen içi | Bileşen içi |
| Ne zaman? | Sık render olan child'larda | Ağır hesaplamalarda | memo'lu child'lara geçilen callback'lerde |
Sonuç
React'te performans optimizasyonu, doğru aracı doğru yerde kullanmayı gerektirir:
React.memo— Bileşenin props'ları değişmediyse yeniden render'ını engeller. Sık render olan parent altındaki child bileşenler için idealdir.useMemo— Pahalı hesaplamaları ve nesne/dizi referanslarını önbelleğe alır. Büyük listelerin filtrelenmesi, sıralanması gibi ağır işlemlerde kullanın.useCallback— Fonksiyon referanslarını sabit tutar.React.memoile birlikte kullanıldığında etkili olur; tek başına kullanmak genellikle anlamsızdır.
Unutmayın: Bu araçların kendileri de bir maliyet taşır (bellek kullanımı ve karşılaştırma işlemleri). Her değişkeni useMemo'ya, her fonksiyonu useCallback'e sarmak yerine, önce React DevTools Profiler ile gerçek darboğazları tespit edin, sonra hedefe yönelik optimizasyon yapın. En iyi optimizasyon, gerçek bir sorunu çözen optimizasyondur.