Sedat Demir

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:

  1. State değişikliği — Bileşenin kendi state'i güncellendiğinde
  2. Props değişikliği — Üst bileşenden gelen props değiştiğinde
  3. Ü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) ifadesi useMemo(() => 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:

  1. Tarayıcıda React DevTools eklentisini açın
  2. Profiler sekmesine geçin
  3. Kayıt başlatıp uygulamada etkileşim yapın
  4. Her bileşenin render süresini ve sıklığını analiz edin
  5. "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.memo ile 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.