Çok Bileşenli Uygulamalarda Render Maliyeti Nasıl Düşürülür?
Bir uygulama yüzlerce bileşenden oluştuğunda, her bileşenin render maliyeti birikimliye dönüşür. Tek bir state değişikliği parent bileşeni güncellediğinde, React varsayılan olarak o bileşenin tüm alt ağacını yeniden render eder. Bu davranış küçük uygulamalarda sorun yaratmaz; ancak bileşen sayısı ve render sıklığı arttıkça gereksiz yeniden hesaplamalar main thread'i bloke eden uzun görevlere dönüşebilir.
Sorun bileşen sayısında değil, gereksiz render'ların sistematik olarak önlenip önlenmediğindedir. Hangi bileşenlerin gerçekten güncellenmesi gerektiğini ayırt etmek ve render'ı yalnızca bu bileşenlerle sınırlı tutmak, hem kullanıcı deneyimini hem de INP değerini doğrudan etkiler. Bu ayrımı yapmak için önce ölçmek, sonra optimize etmek gerekir.
Gereksiz re-render'ı tespit etmek optimizasyonun ilk adımıdır
React DevTools Profiler, her render döngüsünde hangi bileşenlerin yeniden render edildiğini ve her render'ın ne kadar sürdüğünü gösterir. "Highlight updates when components render" seçeneği aktifleştirildiğinde, bir etkileşim sırasında güncellenen bileşenler ekranda renkli bir çerçeveyle işaretlenir. Beklenmedik bileşenler bu görünümde dikkat çeker: bir butona tıklandığında neden sayfanın üst kısmındaki bir navigasyon bileşeni de güncelleniyor?
Bu sorunun iki yaygın kaynağı vardır. Birincisi, üst bileşenden geçen props'un referans kararlılığı olmadan her render'da yeniden oluşturulmasıdır — özellikle nesne ve fonksiyon props'ları için bu durum sıkça görülür. İkincisi, Context API'sinin tüm tüketicileri etkileyen geniş kapsamlı güncellemeler üretmesidir. DevTools Profiler'da bir bileşenin "rendered because parent rendered" yerine "rendered because props changed" yazan render nedenini görmek, asıl sorunun nerede olduğunu netleştirir.
Profil almadan optimizasyon yapmak, hangi kodu hızlandıracağınızı bilmeden optimize etmek anlamına gelir. React.memo, useMemo ve useCallback güçlü araçlardır; ancak yanlış yerde kullanıldığında karmaşıklığı artırır ve bazen performansı da kötüleştirir. Ölçüm, hangi bileşenin gerçekten sorun yaşadığını kanıtlamalıdır.
React.memo yüzeysel karşılaştırma ile render kararını bileşene bırakır
React.memo bir bileşeni sarar ve props değişmediği sürece yeniden render'ı atlar. Karşılaştırma yüzeyseldir: her prop için referans eşitliği kontrol edilir. İlkel değerler (string, number, boolean) için bu yeterlidir; ancak nesne ve fonksiyon props'ları için her render'da yeni referans üretilmesi React.memo'yu etkisiz kılar.
// Her render'da yeni referans — React.memo çalışmaz
function Parent() {
const config = { theme: 'dark' }; // yeni nesne her seferinde
return <Child config={config} />;
}
// useMemo ile sabit referans
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), []);
return <Child config={config} />;
}
Fonksiyon props'ları için aynı sorun geçerlidir. Parent bileşen her render edildiğinde içinde tanımlanan fonksiyon yeni bir referans alır. Bu fonksiyon React.memo ile sarılmış bir child'a geçirilirse, memo koruyucu olmaktan çıkar çünkü prop her seferinde "değişmiş" görünür. useCallback bu durumda fonksiyonun referansını bağımlılıklar değişmediği sürece sabit tutar.
React.memo'nun özellikle değerli olduğu durumlar bellidir: sık render edilen parent'lardan props alan, kendi render'ı maliyetli olan ve props'ları çoğunlukla değişmeyen bileşenler. Liste öğeleri, kart bileşenleri ve izole dashboard widget'ları bu profile uygundur. Her bileşeni memo ile sarmak ise farklı bir hatadır; bellek kullanımını artırır ve karşılaştırma maliyeti eklediğinden seyrek render edilen basit bileşenlerde net kayba yol açar.
useMemo hesaplama maliyetini, useCallback fonksiyon referansını sabitler
useMemo pahalı bir hesaplamanın sonucunu önbelleğe alır ve bağımlılıklar değişene kadar saklı tutar. Ancak "pahalı hesaplama" eşiği çoğu zaman yanlış değerlendirilir. Basit bir filtreleme veya dizi dönüşümü için useMemo kullanmak, önbellekleme mekanizmasının kendi maliyetini karşılamayabilir. useMemo'nun gerçekten değer ürettiği durumlar büyük veri kümeleri üzerinde yoğun hesaplama gerektiren işlemlerdir: ağır sıralama algoritmaları, binlerce öğe üzerinde filtreleme, karmaşık türev veri hesaplamaları.
Bağımlılık dizisi yönetimi de dikkat gerektirir. Bağımlılık listesine fazladan değer eklenmesi önbelleği daha sık geçersiz kılar ve useMemo'nun etkisini azaltır. Eksik bağımlılık ise eski bir değer döndürülmesine — stale closure — neden olur. React'ın ESLint eklentisi bağımlılık dizisindeki eksiklikleri otomatik olarak uyarır ve bu kontrolü atlamak tutarsız UI davranışlarına kapı aralar.
useCallback ise öncelikle React.memo ile sarılmış bileşenlere fonksiyon prop geçildiğinde anlam kazanır. Bağımsız olarak kullanıldığında — yani memoized bir child bileşen yoksa veya fonksiyon başka bir hook'un bağımlılığı değilse — useCallback yalnızca karmaşıklık ekler. Bu araçların doğru bağlama yerleştirilmesi, kör bir şekilde uygulanmasından çok daha etkilidir.
Bileşen ağacını sığlaştırmak render yayılmasını yapısal olarak kısıtlar
Optimizasyon araçlarına başvurmadan önce bileşen hiyerarşisinin kendisi gözden geçirilmelidir. State'in bileşen ağacında gereksiz yüksekte tutulması, o state her güncellendiğinde büyük bir alt ağacın render edilmesine neden olur. "State'i aşağı it" (state colocation) prensibi bu soruna yapısal bir çözüm sunar: state'i yalnızca onu kullanan bileşene veya en yakın ortak parent'a taşımak, güncellemenin etkisini küçük bir alt ağaçla sınırlar.
Context API kullanımında da benzer bir yapısal karar söz konusudur. Tek bir büyük Context nesnesi sık güncellenen ve seyrek güncellenen değerleri bir arada barındırdığında, seyrek değişen bir bileşen sık güncellenen değerler nedeniyle gereksiz yere render edilir. Bağlamı küçük parçalara bölmek — örneğin tema ve kullanıcı tercihlerini ayrı Context nesnelerinde tutmak — her tüketicinin yalnızca ilgilendiği değişikliklere tepki vermesini sağlar.
Bileşen kompozisyonunda "children as props" pattern'i de render yayılmasını kısıtlamanın etkili bir yoludur. Bir parent bileşen yeniden render edildiğinde, children prop'u olarak geçirilen alt bileşenler parent'ın render döngüsünden bağımsız kalır; çünkü bu bileşenler parent'ın dışında tanımlanmış ve referansları değişmemiştir. Bu yaklaşım özellikle sık güncellenen wrapper bileşenlerinde gereksiz re-render'ı önler.
Uzun listeler için virtualization render edilen DOM düğümünü sınırlar
Binlerce öğe içeren bir listenin tamamını DOM'a eklemek hem bellek kullanımını artırır hem de DOM boyutunun getirdiği stil hesaplama ve layout maliyetini tetikler. Kullanıcı ekranında aynı anda yalnızca onlarca öğe görüyor olsa da, geri kalan tüm öğeler DOM'da mevcuttur ve her güncelleme döngüsünde hesaba katılır.
List virtualization bu sorunu çözer: görünür alandaki öğeler ve küçük bir tampon bölge dışındaki öğeler DOM'a eklenmez, yalnızca görünüm penceresine girenler render edilir. react-window ve react-virtual bu yaklaşımın yaygın kütüphane uygulamalarıdır. Pratik sonuç belirgindir: 10.000 öğeli bir listede yalnızca 30–50 DOM düğümü aktif olduğunda, hem bellek kullanımı hem scroll performansı dramatik biçimde iyileşir.
Virtualization her liste için gerekli değildir. Onlarca öğe içeren bir liste için bu karmaşıklığı eklemek gereksizdir. Ancak liste büyüdükçe — özellikle her öğenin kendi iç state'i veya event handler'ı olduğu durumlarda — virtualization tartışmasız bir kazanım sağlar. content-visibility: auto CSS direktifi de benzer bir etki üretmek için kullanılabilir; ancak bu yaklaşım DOM'da tüm öğeleri tutar ve yalnızca görsel render'ı erteler; bellek avantajı sunmaz.
Render maliyetini düşürmek için kullanılan araçların ortak bir ön koşulu vardır: doğru bileşenin, doğru zamanda, doğru araçla ele alınması. Optimizasyon kararları veri ile desteklenmeli; sezgiyle değil, profil çıktısıyla yönlendirilmelidir. React.memo, useMemo ve useCallback, profil verisi olmadan eklendiklerinde kodu karmaşıklaştırır ve bakım yükünü artırır; ölçüm yapılmış gerçek bir sorunun üzerine uygulandığında ise somut ve sürdürülebilir kazanım üretir. Bileşen ağacını sığlaştırmak ve state'i doğru yerde tutmak ise bu araçlardan önce gelir; çoğu durumda yapısal bir düzenleme memoization ihtiyacını tamamen ortadan kaldırır.
Lazy initialization, render maliyetini düşürmenin başka bir boyutunu temsil eder. useState hook'una geçirilen başlangıç değeri her render'da hesaplanır; ancak yalnızca ilk render'da kullanılır. Başlangıç değeri pahalı bir hesaplamanın sonucuysa — örneğin localStorage'dan okuma, büyük bir listeyi filtreleme veya karmaşık bir varsayılan yapı oluşturma — her render bu hesaplamayı tekrarlar. Başlangıç değeri yerine başlatıcı fonksiyon geçirildiğinde React yalnızca ilk render'da bu fonksiyonu çağırır:
// Her render'da çalışır — pahalı başlangıç
const [data, setData] = useState(expensiveComputation());
// Yalnızca ilk render'da çalışır
const [data, setData] = useState(() => expensiveComputation());
Bu fark küçük görünebilir; ancak söz konusu bileşen sık render ediliyorsa ve başlangıç hesaplamasi gerçekten maliyetliyse, kazanım ölçülebilir olur. Lazy initialization özellikle büyük form yapılandırmalarını, kompleks varsayılan state ağaçlarını veya depodan okunan değerleri başlangıç state'i olarak kullanan bileşenlerde değerini gösterir.
Bileşen düzeyinde dynamic import ile lazy loading de render maliyetini farklı bir açıdan yönetir. Uygulamanın başlangıçta ihtiyaç duymadığı bileşenleri — modal içerikleri, gelişmiş filtre panelleri, yalnızca belirli izinlere sahip kullanıcıların gördüğü bölümler — React.lazy ve Suspense ile isteğe bağlı olarak yüklemek, hem başlangıç bundle boyutunu küçültür hem de bu bileşenlerin parse maliyetini gerçekten ihtiyaç duyulana kadar erteler. Render optimizasyonu ile kod bölme stratejisi bu noktada birbirini tamamlar.