React.memo her bileşene eklenmesi gereken bir optimizasyon değildir. Props karşılaştırmasının maliyeti, referans kararsızlığı ve context tüketimi memo'yu çoğu durumda etkisiz kılar. Profiler verisiyle desteklenen, yalnızca render maliyeti yüksek bileşenlere uygulanan memo gerçek fark yaratır.
React.memo Ne Zaman Gereksiz, Ne Zaman Şart?
React.memo, bir bileşenin props'ları değişmediğinde yeniden render edilmesini önleyen bir sarmalayıcı fonksiyondur. İlk bakışta her bileşene uygulanması gereken bir optimizasyon gibi görünür; oysa çoğu projede ya gereksiz kalmakta ya da yanlış kullanılmaktadır. Anlık render maliyeti düşük bileşenlerde memo, çözdüğünden daha fazla karmaşıklık üretir.
Yeniden render, React'in temel çalışma mekanizmasıdır. Parent bileşen her güncellendiğinde child bileşenler de yeniden çalışır. Peki bu çalışma gerçekten pahalı mıdır? Cevap bileşene, DOM ağacının derinliğine ve hesaplama yüküne göre değişir; memo bu soruyu sormadan kullanıldığında getiriden çok gider üretir.
Props karşılaştırması ücretsiz değildir. React.memo, her render döngüsünde önceki ve mevcut props'ları sığ biçimde karşılaştırır. Bu işlemin maliyeti genellikle küçüktür, ancak yüzlerce küçük bileşene uygulandığında toplam yük fark edilir biçimde artabilir. Daha önemlisi, referans kararlılığı sağlanmadan yapılan her memo uygulaması fayda sunmadan maliyet üretir.
Yeniden render neden her zaman sorun değildir
React, virtual DOM farklaştırma ile gerçek DOM güncellemelerini minimize eder. Bir bileşen yeniden render edildiğinde bu hemen DOM'a yansımaz; React önce virtual DOM ağacını günceller, sonra gerçek DOM ile karşılaştırır ve yalnızca değişen düğümleri yazar. Bu süreç, DOM manipülasyonunun tarayıcı açısından maliyetli olduğu dünyada oldukça verimli bir çözümdür.
Basit bir li elemanı ya da birkaç paragraf içeren küçük bir kart bileşeni için tüm bu süreç mikrosaniyeler mertebesindedir. Yüzlerce küçük bileşen aynı anda render edilse bile toplam süre çoğu durumda 16 ms eşiğinin altında kalır; kullanıcı herhangi bir takılma ya da gecikme hissetmez.
Sorun, bir bileşen içinde ağır hesaplama yapıldığında, büyük bir dizi üzerinde işlem yürütüldüğünde ya da derin iç içe geçmiş nesne yapıları oluşturulduğunda ortaya çıkar. O noktada render maliyeti ölçülebilir biçimde yükselir ve optimizasyon anlam kazanır. Profiler olmadan eklenen memo ise çözülecek bir problemi aramak için eklenen bir yükten ibarettir.
Render sayısını sıfıra indirmek hedef değildir. Render hızlı olduğu sürece sık render edilmesi sorun çıkarmaz; asıl hedef her render'ın hızlı kalmasını sağlamaktır.
memo'nun yaptığı iş ve karşılaştırmanın gerçek maliyeti
React.memo(Component) çağrısı, bileşeni bir Higher-Order Component (HOC) içine sarar. Her render döngüsünde önceki props ile yeni props'ı sığ biçimde karşılaştırır; karşılaştırma Object.is semantiğine dayanır: iki değer aynı referansı paylaşıyorsa eşit kabul edilir, aksi takdirde farklıdır.
Bu kural, inline fonksiyon ve nesne tanımlarını ciddi biçimde sorunlu kılar. Parent bileşen her render edildiğinde onClick={() => handleClick(id)} gibi bir fonksiyon yeni bir referans üretir. React.memo bu iki fonksiyonu farklı görür ve bileşeni yine de render eder; karşılaştırma maliyeti boşa gitmiş olur, koruma sağlanamamış olur.
Nesneler de aynı sorunu yaşar. style={{ color: 'red' }} ya da options={{ page: 1 }} gibi prop'lar her render'da yeni bir nesne referansı oluşturur; memo bu değişikliği gerçek bir prop değişimi olarak yorumlar ve bileşeni yeniden çizer. Yüzeysel eşitliği geçemeyen referanslar, memo'yu işlevsiz kılar.
Karşılaştırma özelleştirilmek isteniyorsa React.memo(Component, areEqual) ikinci argüman alabilir. Bu fonksiyon yanlış yazıldığında ya çok az ya da çok fazla render'ı önler; ikincisi stale props sorununa yol açar ve bileşen eski değerlerle çalışmaya devam eder. Özel karşılaştırıcı, basit durumda fazladan karmaşıklık üretmeksizin sonuç vermez.
memo'nun gereksiz kaldığı durumlar
Birkaç string ya da number props alan, hiçbir hesaplama yapmayan bileşenlerde memo'nun sağladığı şey yoktur. Karşılaştırma maliyeti render maliyetiyle yaklaşık dengededir, net kazanç sıfıra yaklaşır. Böyle bir bileşene memo eklemek, ileride props değiştirmek istediğinizde bileşenin neden güncellenmiyor diye araştırma yapma riskini beraberinde getirir.
Context tüketen bileşenler memo ile korunmaz. useContext aracılığıyla bir değer okuyan bileşen, context güncellendiğinde her zaman yeniden render edilir; props değişmemiş olsa bile. Context değişimi memo'yu atlatır. Sık güncellenen bir context varsa ve bu context birçok bileşen tarafından tüketiliyorsa, çözüm memo değil, context'i parçalamak ya da seçici abonelik sağlayan bir state yönetim yaklaşımıdır.
State'i doğrudan kendi içinde tutan bileşenler de memo'dan yararlanamaz. Kendi state'i değişince bileşen her koşulda render edilir; memo yalnızca parent kökenli yeniden render'ları engeller, bileşenin kendi state güncellemelerini değil. Bileşen içinde her saniye değişen bir zamanlayıcı ya da sürekli güncellenen bir kullanıcı girişi varsa memo neredeyse hiçbir zaman devreye giremez.
Her bileşene savunma amaçlı memo eklemek gerçek bir kazanç yerine, okunması ve bakımı güç bir kod tabanı üretir. Her memo sarmalayıcısı, gelecekteki bir props değişikliğinin bileşeni güncelleyip güncellemeyeceğini takip etme yükü getirir. Referans kararsızlığı varken eklenen memo ise yalnızca kodu karmaşık hale getirir, render'ları durdurmaz.
memo'nun gerçek fark yarattığı durumlar
Props nadiren değişen ve hesaplama maliyeti yüksek bileşenler, memo'nun en doğal kullanım alanıdır. Büyük bir veri kümesini filtreleyen ya da sıralayan bir liste satırı, her render'da derin bir DOM ağacı oluşturan hiyerarşik bir bileşen ya da canvas üzerine çizen bir grafik; bunlar render maliyeti gerçekten ölçülebilir olan bileşenlerdir.
Yüzlerce satırdan oluşan tablolar somut bir örnektir. Tablonun üstünde bir arama kutusu varsa her tuş vuruşunda parent bileşen güncellenir. Satır bileşeni memo ile sarmalanmış ve satır verisi değişmemişse, o satır render edilmez; arama kutusu her tuş vuruşunda tüm tabloyu yeniden çizmek yerine yalnızca kendi state'ini günceller. Binlerce satır için bu fark kullanıcı arayüzünün akıcılığına doğrudan yansır.
Ağırlıklı olarak saf hesaplama yapan bileşenler de güçlü bir adaydır. Bir ürün listesini fiyat aralığına ve kategoriye göre filtreleyen, sonuçları sıralayan ve toplamları hesaplayan bir bileşen; aynı giriş verileri için her seferinde aynı çıktıyı üretir. Props değişmediğinde bu hesaplamaları tekrar yapmak gereksizdir.
Memo'nun işe yaraması için üç koşulun birden sağlanması gerekir: bileşenin render maliyeti ölçülebilir biçimde yüksek olmalı, props'lar sık değişmemeli ve parent bileşenin güncelleme döngüsü sık olmalıdır. Bu üç koşul eş zamanlı oluşmadığında memo sınırlı fayda sağlar.
React Profiler ile ölçmek
Tahminden önce ölçmek şarttır. React DevTools'taki Profiler sekmesi, her bileşenin render süresini milisaniye cinsinden gösterir; hangi bileşenin ne kadar zaman harcadığını flamegraph görünümünden takip edebilirsiniz. Bu ölçüm olmadan yapılan optimizasyon büyük ölçüde tahmine dayalı kalır.
Profiler'ı açın ve gerçek kullanım senaryosunu tetikleyin. Bir buton tıklamasında, liste filtrelemesinde ya da form güncellemesinde hangi bileşenler en uzun render süresini gösteriyor? Eğer incelenen bileşen 1 ms altında render ediliyorsa, memo eklemenin render süresine ölçülebilir etkisi olmayacaktır. Sonuç ilk önce gelir: ölçüm memo'nun etkili olup olmadığını söyler, tartışma değil.
React.memo eklendikten sonra aynı senaryoyu tekrar çalıştırın ve render sayısını karşılaştırın. Render sayısı düşmüyorsa referans kararsızlığı vardır; props olarak geçilen fonksiyon ya da nesne her döngüde yeniden oluşturuluyor demektir. Bu durumda memo'dan önce referans kararlılığını sağlamak gerekir.
Profiler ayrıca "why did this component render?" bilgisini de sunar. Hangi prop'un değiştiğini gösteren bu panel, memo eklemeden önce sorunun kaynağını doğru tespit etmenizi sağlar. Zaman harcayan bileşeni bulmak, o bileşene çözüm uygulamaktan daha önemlidir.
useCallback ve useMemo ile referans kararlılığı
React.memo yalnızca referans kararlı props'larla anlamlı çalışır. Fonksiyon prop'larını kararlı kılmak için useCallback, hesaplanmış nesne ya da dizi prop'larını kararlı kılmak için useMemo kullanılır. Bu üçü birlikte çalışır; birini kullanıp diğerini ihmal etmek memo'yu etkisiz bırakır.
useCallback(fn, deps) verilen bağımlılıklar değişmediği sürece fonksiyon referansını korur. Parent bileşen içindeki bir event handler her render'da yeniden oluşturuluyorsa ve bu handler memo ile sarmalanmış bir child'a prop olarak geçiliyorsa, useCallback olmadan memo hiçbir zaman devreye girmez.
Ancak useCallback ve useMemo da ücretsiz değildir. Her ikisi de bağımlılık dizisini her render'da karşılaştırır ve önceki değeri belleğe alır. Basit hesaplamaları useMemo içine sarmak, hesaplamanın kendisinden daha yavaş olabilir. Kural basittir: eğer memo'suz performans sorunsuzu işliyorsa, callback ve memo da gereksizdir.
Üçlünün birlikte kullanıldığı en verimli senaryo şudur: sık güncellenen bir parent, nadiren değişen ağır bir child, ve child'a geçilen fonksiyon ile nesne prop'ları useCallback ile useMemo ile kararlı hale getirilmiş. Bu yapıda memo gerçek anlamda koruma sağlar; aksi durumda üç katmanlı bir karmaşıklık oluşur ve hiçbir şey durdurulmaz.
Karar kriterleri: ne zaman eklemek, ne zaman çıkarmak
Memo ekleme kararı üç soruya dayanır. İlki: bu bileşenin render maliyeti Profiler'da görünür mü? İkincisi: bu bileşen sık güncellenen bir parent'ın altında mı? Üçüncüsü: props'ları gerçekten nadiren değişiyor mu ve referans kararlılığı sağlanmış mı? Üçüne de evet yanıtı verilebiliyorsa memo makul bir seçimdir. Bir tanesi bile hayır ise, önce o sorun çözülür.
Memo çıkarma kararı da eşit önem taşır. Kod tabanında eski bir optimizasyon amacıyla eklenmiş, artık o bileşene çok sayıda dinamik prop geçilen ya da yoğun context tüketimi yapılan memo sarmalayıcıları performansa katkı değil, okunabilirliğe zarar verir. Bu tür memo'ları temizlemek, ekipte gereksiz karmaşıklık bırakmamak açısından değerlidir.
Listedeki her satır bileşenini memo'lamak bir refleks haline gelirse, veri yapısı değiştiğinde neden bazı satırlar güncellenmiyor sorusu kaçınılmaz olarak ortaya çıkar. Referans tutarsızlıkları takip edildiğinde hata ayıklama süresi uzar. Memo'nun kazandırdığı milisaniyeler, bu karmaşıklığın mühendislik saatlerine maliyetiyle kıyaslandığında tablo çoğunlukla memo aleyhine döner.
Büyük ölçekli uygulamalarda memo stratejik olarak uygulanır: ağır liste satırları, seyrek güncellenen panel bileşenleri, hesaplama yoğun grafikler. Her yere yayılan memo değil, gerektiği yerde, ölçüm sonrasında eklenen memo kalıcı fayda üretir.
Performans optimizasyonu ölçümle başlar, varsayımla değil. React.memo güçlü bir araçtır; ancak gücü, her bileşene değil doğru bileşene uygulanmasından kaynaklanır. Profiler'ın gösterdiği gerçek dar boğazlar üzerinde çalışmak, sezgiyle yapılan optimizasyondan her zaman daha güvenilir sonuç verir.
Referans kararlılığı sağlanmadan eklenen her memo, ileride "bu neden güncellenmiyor?" sorusunu doğurur. useCallback ve useMemo ile birlikte kullanılan, Profiler verisiyle desteklenen ve yalnızca maliyeti gerçekten yüksek bileşenlere uygulanan memo, beklenen etkiyi gösterir.
Kod tabanında memo ile karşılaşıldığında sormaya değer bir soru vardır: bu bileşenin render maliyeti ölçülmüş müdür? Yanıt bilinmiyorsa, o memo'nun varlığı da sorgulanabilir. Ölçüm olmadan eklenen optimizasyon, iyi niyetle yazılmış teknik borçtan farklı değildir.