useMemo ve useCallback Nerede Gereksiz Hale Gelir?
useMemo ve useCallback her hesaplamayı veya fonksiyonu sarmalamak için değildir. Memoization'ın kendi maliyeti var; basit hesaplamalara, referansı kimsenin kontrol etmediği fonksiyonlara ve küçük dizilere eklendiğinde kazanç değil ek yük üretir.
Bir kod tabanında "performans için" eklenen useMemo ve useCallback satırlarının sayısı zamanla artıyor, ama Profiler'da render süreleri aynı kalıyor, hatta bazı bileşenlerde hafifçe kötüleşiyor. Ekip her yeni fonksiyonu useCallback'e, her hesaplamayı useMemo'ya sarmayı bir alışkanlık haline getirmiş, ancak bu alışkanlığın karşılığında ölçülebilir bir kazanç yok. Aksine bundle biraz büyümüş, dosyalar biraz daha kalabalıklaşmış, kod okurken her satırda "bu neden memoize edilmiş?" sorusu sorulur olmuş.
Bu tablo tesadüf değil. useMemo ve useCallback'in kendisi ücretsiz değildir; her ikisi de bir önceki render'ın bağımlılıklarını saklar, karşılaştırma yapar ve gerekirse yeni değeri hesaplar. Bu iş yükü, memoize edilen hesaplamanın maliyetinden düşükse net kazanç sıfırın altına iner. Sorun genelde "memoization iyi bir şeydir" genellemesinin, "bu spesifik hesaplama gerçekten pahalı mı" sorusunun önüne geçmesinden doğuyor.
Aşağıda hangi durumlarda bu iki hook'un ek yük ürettiği, hangi durumlarda gerçekten kazandırdığı ve aradaki farkı nasıl ayırt edeceğiniz sırayla ele alınıyor.
Memoization'ın kendisi de ücretsiz değildir
useMemo(fn, deps) çağrıldığında React, bağımlılık dizisini bir önceki render'daki diziyle karşılaştırır. Bu karşılaştırma Object.is ile dizi elemanı başına yapılır; dizi ne kadar uzunsa karşılaştırma o kadar iş yapar. Değer değişmemişse önceki sonuç bellekten döndürülür, değişmişse fonksiyon yeniden çalıştırılır ve yeni sonuç saklanır. Bu saklama işlemi de bellek ayırır; bileşen her render'da bu kaydı taşır.
Basit bir toplama işlemi için bu maliyet hesaplamanın kendisinden daha büyüktür:
function OrderSummary({ price, quantity }) {
// toplam maliyet: bir çarpma işlemi, birkaç nanosaniye
const total = useMemo(() => price * quantity, [price, quantity]);
return <p>Toplam: {total}</p>;
}
price * quantity işleminin kendisi CPU'da ölçülemeyecek kadar hızlıdır. useMemo burada iki değeri karşılaştırmak, bir closure oluşturmak ve sonucu saklamak için harcadığı işi, kaydedeceği hesaplama süresinden fazlasına mal eder. Fark tek bir bileşende görünmez, ama yüzlerce bileşende bu desen tekrarlandığında birikir; her biri kendi başına anlamsız, toplamda ölçülebilir bir ek yük.
Hangi hesaplamalar useMemo'ya hiç ihtiyaç duymaz?
Pratik bir eşik şudur: hesaplama, JavaScript motorunun bir render döngüsünde fark edilmeden yapabileceği kadar basitse, memoization eklemek yalnızca kod karmaşıklığı üretir. Bu kategoriye giren tipik örnekler:
// Gereksiz: basit string birleştirme
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
// Gereksiz: küçük dizide filtre (birkaç yüz öğeye kadar)
const activeItems = useMemo(
() => items.filter(i => i.active),
[items]
);
// Gereksiz: koşullu sınıf adı üretimi
const className = useMemo(
() => (isActive ? 'btn active' : 'btn'),
[isActive]
);
Bu üç örneğin ortak noktası, işlemin doğrusal zaman karmaşıklığında ve küçük veri boyutunda çalışmasıdır. Birkaç yüz öğelik bir diziyi filtrelemek modern donanımda mikrosaniyeler sürer; bu süre, tarayıcının bir render döngüsüne ayırdığı bütçenin (16 ms hedefinde) ölçülemeyecek kadar küçük bir dilimidir. Buraya useMemo eklemek, kazanılmayan bir performansın karşılığında kod satırı ve bilişsel yük satın almaktır.
Eşik nerede değişir? Dizi binlerce öğeye çıktığında, işlem iç içe döngü veya nesne dönüşümü içerdiğinde, ya da her render'da tekrar çalıştırılması gözle görülür bir gecikme yarattığında. Karar bu eşikte sezgiyle değil, ölçümle verilmelidir; bir sonraki bölüm bunu ele alıyor.
useCallback'in faydası, kimsenin referansı kontrol etmediği yerde sıfırdır
useCallback, useMemo'dan farklı bir amaca hizmet eder: hesaplama sonucunu değil, fonksiyon referansını sabitler. Bu sabitleme yalnızca şu iki durumdan biri geçerliyse anlam kazanır: fonksiyon React.memo ile sarılmış bir alt bileşene prop olarak geçiyor, ya da fonksiyon başka bir hook'un bağımlılık dizisinde kullanılıyor.
function ParentList({ items }) {
const [selected, setSelected] = useState(null);
// ItemRow React.memo ile sarılı DEĞİLSE bu useCallback anlamsızdır
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);
return items.map(item =>
<ItemRow key={item.id} item={item} onSelect={handleSelect} />
);
}
// React.memo yoksa, ItemRow her ParentList render'ında
// zaten yeniden render olur; handleSelect'in referansı
// sabit olsun ya da olmasın fark etmez
function ItemRow({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
}
Bu örnekte ItemRow, React.memo ile sarılmadığı için zaten her ebeveyn render'ında yeniden render olur. handleSelect'in referansının sabit kalması hiçbir render'ı önlemez; useCallback burada tamamen boşa harcanmış bir karşılaştırma ve closure maliyetidir. Aynı fonksiyon ItemRow memoize edildiğinde ise durum tersine döner: sabit referans olmadan React.memo'nun karşılaştırması her seferinde "değişti" der ve memoization'ın kendisi devre dışı kalır. React.memo'nun ne zaman gerçekten işe yaradığını bilmeden useCallback kararı vermek, birbirine bağlı iki kararı ayrı ayrı ve yanlış vermek anlamına gelir.
İkinci geçerli kullanım, fonksiyonun bir useEffect bağımlılık dizisinde yer almasıdır. Fonksiyon her render'da yeniden oluşturuluyorsa ve effect'in bağımlılık listesine giriyorsa, useCallback olmadan effect gereksiz yere sürekli tetiklenir. Bu senaryo, yanlış kurulmuş bağımlılık listelerinin ürettiği sorunlardan biriyle doğrudan kesişir; referans kararsızlığı burada da kök nedendir.
Her satıra sarmalama eklemenin bundle ve okunabilirlik maliyeti
useMemo ve useCallback'in çalışma zamanı maliyeti tek başına küçüktür, ama bu iki hook'u proje genelinde varsayılan alışkanlık haline getirmenin ikinci bir bedeli var: her çağrı ekstra bir fonksiyon tanımı, bir bağımlılık dizisi ve bir closure demektir. Bu, minifikasyon sonrası bile tamamen sıfırlanmayan bir kod hacmi artışıdır; parse ve execute aşamasında tarayıcının işlemesi gereken satır sayısı büyür.
Okunabilirlik tarafındaki maliyet daha somuttur. Bir bileşende yirmi satırlık mantığın on ikisi useMemo ve useCallback sarmalayıcısıysa, gerçek iş mantığı bu sarmalayıcıların arasında kayboluyor demektir. Kod incelemesi yapan biri her satırda "bu bağımlılık dizisi doğru mu, bu hesaplama gerçekten pahalı mı" sorularını sormak zorunda kalır; bu, projeye gerçek bir performans kazancı olmadan bilişsel yük ekler. Takım büyüdükçe bu yük, yeni katılan geliştiricilerin "her şeyi memoize etmeliyim" yanlış varsayımını miras almasına yol açar.
Gerçekten kazandıran iki profil
Memoization'ın net kazanç ürettiği durumlar dar ama net bir çerçeveye oturur. Birincisi, hesaplama maliyeti gerçekten yüksek olduğunda: büyük bir diziyi sıralamak, iç içe döngüyle veri dönüştürmek, ağır bir regex çalıştırmak veya bir grafiği yeniden hesaplamak gibi işler render başına milisaniyeler sürebilir.
function AnalyticsTable({ rows }) {
// 10.000+ satırlık veride çoklu alan bazlı sıralama ve gruplama
const processedRows = useMemo(() => {
return rows
.filter(r => r.status === 'active')
.sort((a, b) => b.revenue - a.revenue)
.reduce((groups, row) => {
(groups[row.category] ??= []).push(row);
return groups;
}, {});
}, [rows]);
return <Table data={processedRows} />;
}
Burada rows değişmediği sürece bu üç zincirli işlemi her render'da tekrarlamak gerçek bir maliyettir; useMemo bu maliyeti yalnızca rows gerçekten değiştiğinde ödetir. Kazanç, doğrudan Profiler'daki commit süresinde görülebilir.
İkinci profil, referans kararlılığının başka bir mekanizmayı beslediği durumdur: React.memo ile sarılmış, sık render edilen bir üst bileşenin altında duran ve kendisi pahalı render'a sahip bir alt bileşen. Bu durumda useCallback ve useMemo, hesaplama maliyetini değil, gereksiz render zincirini keser. Bu ikinci profil, render maliyetini yapısal olarak düşürme stratejisinin bir parçası olarak değerlendirilmeli; tek başına bir hook eklemek değil, bileşen ağacındaki hangi düğümün gerçekten sık ve pahalı render olduğunu bilerek karar vermek gerekir.
Ölçmeden karar vermemek: doğrulama adımları
Bir useMemo veya useCallback eklemeden önce sorulması gereken soru nettir: bu hesaplama veya bu render, ölçülebilir bir gecikme üretiyor mu? Bu sorunun cevabı sezgiyle değil, React DevTools Profiler'daki commit sürelerine bakılarak verilmelidir. Bir bileşenin render süresi zaten 1 ms altındaysa, o bileşene memoization eklemenin kazandıracağı bir şey yoktur; kazandırdığını iddia etmek ölçüm yapmadan konuşmaktır.
Pratik doğrulama sırası şöyle işler: önce şüpheli bileşeni Profiler ile kaydedin, render süresini ve render sıklığını not edin. Sonra memoization ekleyin ve aynı senaryoyu tekrar kaydedin. Süre gözle görülür şekilde düşmüyorsa, eklenen kod geri alınmalıdır; performans kazancı olmayan karmaşıklığı kod tabanında tutmanın hiçbir gerekçesi yoktur. Main thread'i bloke eden gerçek darboğazlar genelde büyük listelerde, ağır hesaplamalarda veya senkron DOM ölçümlerinde bulunur; küçük aritmetik işlemlerde değil.
Bu ölçüm disiplini, ekip büyüdükçe daha da önemli hale gelir. "Performans için ekledim" gerekçesi, bir before/after ölçümüyle desteklenmediği sürece kod incelemesinde kabul edilmemeli. Aksi halde memoization, gerçek bir optimizasyon aracından proje genelinde birikip yavaşça bundle'ı ve okunabilirliği aşındıran bir alışkanlığa dönüşür.