Memoization Nedir? React.js’te Nasıl Uygulanır?
Memoization(Hafızada Tutma) Nedir ?
İçgüdüsel olarak maliyeti yüksek ancak tekrarlı olarak yapmamız gereken bir işlem olduğunda tekrar sayısını azaltıcı ve maliyetleri düşürücü çeşitli en iyileme yöntemleri bulmaya çalışırız.
Cacheleme(geçici hafızada tutma) işlemi ile maliyeti yüksek işlemleri hafıza tutarak tekrar gereksinim duyduğumuzda hesaplama yükünden sıyrılarak daha hızlı bir işlem gerçekleştirebiliriz. Bir yandan hafızada tutma işlemi ek bir performans yükü getirirken diğer yandan yüksek maliyetli hesaplamadan kaçınma kararı vermek oldukça hassastır. Bu nedenle tercihlerimizi bu iki seçenek arasında yaparken dikkatli olmamız gerekir.
React özellinde bu işlemin nasıl yapılacağına geçmeden önce bilmemiz gereken iki temel konsepte değinmekte fayda var.
- React Render Mekanizması:
Bir kullanıcı etkileşimi ya da uygulama verisinde meydana gelen çeşitli işlemler neticesinde bileşenlerde bir takım değişimler meydana gelebilir. React bu değişiklikleri önceki haliyle karşılaştırır ve optimize edecek şekilde değişlikleri uygular. Detaylı inceleme için bakınız.
Bileşenlerin tekrar renderlama(re-render) kararı vermesi için bileşenin propları ve statelerinde ya da elementlerin key özelliğinde değişim gözlemlenmesi gerekir. Ayrıca eğer parent element renderlanırsa child elementlerin de doğal olarak bu süreçte renderlanacağı akılda tutulmalıdır.
- Javascript Referans Eşitliği(Referential equality) Kavramı:
Javascript’te object(nesne), array(dizi) ve function(işlev) referans türünde tutulan veri tipleridir. Esas olarak JS’te array ve function özel bir object türü olarak görüldüğünden object türünün hafızada bulunduğu konuma göre eşit ya da eşit olmamak yönünde bir sonuç ortaya çıkarır.
Yukarıda bahsedilen renderlama sürecinde her nesne hafızada farklı bir referansa atanacak şekilde yeniden oluşturulur. İşte bu noktada referansı hafızada tutma kararı verilmesi gerekli hale gelecektir.
React.Js’te Nasıl Uygulanır ?
React.js’te bu işlemi uygulamak için temel olarak useCallback, useMemo hookları ve memo API’si karşımıza çıkar.
Detaylarına ve kullanım uygulamalarına geçmeden önce iki konuda uyarıda bulunmakta fayda olduğu görüşündeyim.
- Bu yapılar yalnızca performans optimizasyonu yapmak amacıyla kullanılmalıdır. Dolayısıyla her fonksiyonu, her değeri ya da her bileşeni cachelemek doğru bir uygulama olmayacaktır. Bunu tercih etmek belli ölçüde projeye hissedilir bir zarar vermeyebilir. Ancak kod karmaşası yaratması yönüyle dikkatli kullanmakta fayda olacaktır.
- Ayrıca ilk renderlama sürecinde fazladan tanımlanan fonksiyonlar ve bunların tetiklenmesi nedeniyle projede gereksiz performans kayıpları yaratabilir. Detaylı inceleme için bakınız.
2. Projede meydana gelen çeşitli hatalar ya da performans zafiyetleri için öncelikle bu durumu giderici işlemler araştırılmalıdır. Bu yapılar ilk çözüm olarak düşünülmemelidir.
- Büyük çaplı projelerde dahi yoğun ölçüde bu yapılar kullanılmadan işlemler gerçeklenebilir. Detaylı inceleme için bakınız.
Peki kullanım durumları nedir ?
Bu yapılar temel olarak iki durum için kullanım durumu ortaya çıkarabilir.
1- Bileşenlerin tekrar renderlanmasını(re-render) engellemek
2- Maliyetli hesaplamalardan kaçınmak
Şimdi sırasıyla bu yapılara bakalım.
* useCallback( )
Tekrar renderlama esnasında fonksiyon tanımlarının cachelenmesini sağlar.
import { useCallback } from 'react';
function XCompanyPage({ containerId, companyName}) {
const handleLoad = useCallback((weight) => { // *Function Definition
post('/container/' + containerId + '/load', {
companyName,
weight,
});
}, [containerId, companyName]); // *Dependencies List
Burada görüleceği gibi bir fonksiyon ve bu fonksiyonun tekrar çağrılıp çağrılmayacağına karar kılan bağlılıklar listesi (dependencies list) bulunmaktadır.
* useMemo( )
Tekrar renderlama esnasında hesaplanan değerin cachelenmesini sağlar.
import { useMemo } from 'react';
function LoadStack({ containers, destination}) {
const loadedPlates = useMemo(
() => filterPlates(containers, destination),
[containers, destination] // *Dependencies List
);
// ...
}
Parametre almayan bir fonksiyon ile hesaplama yapılır ve bu hesaplamanın tekrar yapılıp yapılmayacağına bağımlılık dizisindeki değerler karar verir.
* memo( )
Bir bileşinin prop’larında değişim olmadığında tekrar renderlama işlemini atlamak amacıyla kullanılır.
import { memo } from 'react';
const MemoizedXCompanyComponent = memo(function XCompanyComponent(props) {
// ...
});
Yukarıda görüleceği gibi bir bileşeni parametre olarak alır ve props değişmemişse tekrar renderlamaya izin vermez. Ayrıca proplarının eşitliğini kontrol amacıyla fonksiyon yazmamızı sağlayan opsiyonel bir parametre de kabul eder.
İki önemli notla genel tanıtım kısmını geçeceğim.
1- useMemo ile her türlü değer cacheleyebilirsiniz. Bu bir değerde olabilir, bir JSX’te hatta bir fonksiyon da olabilir. Fonksiyonda cacheleyebiliyorsa neden useCallback var diyor olabilirsiniz.
Aslında fazladan bir iç içe geçmiş(nested) fonksiyon yazmaktansa useCallback kullanımı fonksiyon cachelenmesi adına doğru bir yöntem ve isimsel olarak daha açıklayıcı bir yaklaşım olacaktır.
2- Bu üçlü yapının ayrı ayrı işlevsellikleri olsa da müşterek kullanımları ile de performans en iyilemesi yapabileceğimizi unutmamalıyız.
Analojik Yaklaşım
Şimdi analojik bir yaklaşımla bu süreci kafamızda daha iyi canlındarmaya çalışalım. Sonrasında bu benzeşmeyi kodlayarak süreci gerçekleyelim. Burada söz konusu ortak noktaları bulmak olduğundan detayları görmezden geleceğim olabildiğince.
Gemilere konteyner yüklenen bir limanı düşünelim. Gerçek anlamda ağır operasyonların icra edildiği böyle bir ortamda hem ağırlıkların hem de vinçlerin operasyonel anlamda optimize edilmesi oldukça önemlidir.
- Gemi ile uygulamanın DOM(Document Object Model)’ini
- Vinçler ile fonksiyonları
- Konteyner ya da konteyner balyaları ile JSX ya da çeşitli değerleri benzeştirelim.
1- Gemi yük alanına, yüklerin indirilmesi esnasında kolaylık sağlayacak şekilde rotaya uygun olarak yükleme yapılması gerekir.
Web uygulamalarındaki kullanıcı işlemlerinin en kolay şekilde yapılmasının sağlaması gibi.
2- O zaman vinçlerin işlevleri ve konumları, ilgili gemi için yüklenecek konteynerler bitene kadar sabit tutulmalıdır. Hem enerji tasarufu hem de operasyonun sürdürülebilmesi adına bu sürecin devam ettirilebilmesi gerekir.
Web uygulamalarındaki belli işlevler performans kaybı yaratıyorsa ve sürecin devam ettirilmesi için hafızadaki konumu cachelemensi kaçınılmazsa burada useCallback( ) ile işlevi ve referansını bağlılık listesindeki değerlerin durumuna göre cacheleme işlemine tabi tutabiliriz.
3- Konteyner bloklarının oldukça ağır olduğunu biliyoruz. Bu blokların en altında bulunan bir konteynerın başka gemiye yükleneceğini hayal edelim.Üstünde bulunan tüm blokların tek tek kaldırılması ve ilgili konteynerın alınarak blokların tekrar istiflemeye tabi tutulacağını düşünün. Bunun için ilk aşamada ilgili gemiye yüklenecek blokları tanımlayıp sabit tutmaya çalışırız.
Web uygulamalarındaki hesaplaması oldukça maliyetli ve tekrarlı hesaplamalarda performans sorunları yaratan bir değeri bu süreç gibi hayal edebiliriz. Bir defa bu hesaplamayı yaptıktan sonra useMemo( ) ile cacheleyerek bağlılık listesinde bulunan bir parametre değişmediği sürece ilgili değeri kullanmaya devam ederiz.
4- Yine hem gemi yük alanında hem de liman hattında bir rotaya gidecek belli tarzda konteyner bloklarını sabit tutarak hem yükleme hem de indirme esnasında kolaylık sağlamış oluruz.
Web uygulamalarındaki bir bileşen ya da bileşen grubunun performans problemi yaratabileceğini gözlemlediğimizde bu element bloklarının sabit kalmasını sağlama açısında memo( ) ile cachelemeye tabi tutmamız gibi.
Şimdi bu süreçte anlattıklarımızı koda dökmeye başlayabiliriz.
import React, { useState, useCallback, useMemo, useRef } from "react";
const ShippingContainer = React.memo(
({ id, weight, type }) => {
return (
<div>
<p>Container ID: {id}</p>
<p>Weight: {weight} tons</p>
<p>Type: {type}</p>
</div>
);
},
(prevProps, nextProps) => {
return prevProps.id === nextProps.id;
}
);
export default function PortOperations() {
const containersRef = useRef([
{ id: 1, weight: 20, type: "Type A" },
{ id: 2, weight: 15, type: "Type B" },
{ id: 3, weight: 25, type: "Type A" },
// ... Diğer konteynerler
]);
const [triggerRender, setTriggerRender] = useState(false);
const removeContainer = useCallback((containerId) => {
containersRef.current = containersRef.current.filter(
(container) => container.id !== containerId
);
setTriggerRender((prev) => !prev);
}, []);
const addContainer = useCallback(() => {
const newId = containersRef.current.length + 1;
const newContainer = { id: newId, weight: 30, type: "Type C" };
containersRef.current = [...containersRef.current, newContainer];
setTriggerRender((prev) => !prev);
}, []);
const totalWeight = useMemo(
() =>
containersRef.current.reduce(
(total, container) => total + container.weight,
0
),
[triggerRender]
);
return (
<div>
<h2>Port Operations - Shipping Containers</h2>
<p>Total Weight: {totalWeight} tons</p>
<button onClick={addContainer}>Add Container</button>
{containersRef.current.map((container) => (
<div key={container.id}>
<ShippingContainer
id={container.id}
weight={container.weight}
type={container.type}
/>
<button onClick={() => removeContainer(container.id)}>Remove</button>
</div>
))}
</div>
);
}
PERFORMANS OPTİMİZASYONU ÖNCESİNDE !!!
React dökümanında da üzerinde durulan aşağıda açıklayacağım maddeler bir çok cacheleme işlemini gereksiz kılabilecek etkiye sahip olan işlemlerdir.
- Eğer bir component yalnızca görsel olarak bir bileşeni barındıyorsa burada JSX’in “chidren” prop’u olarak alınması(JSX as children) parent bileşenin state’inde değişiklik olsa bile React bu bileşenin state değişiminden etkilenmeyeceğini varsayarak tekrar renderlama yapmaması performans yönüyle etkili olacaktır.
- State’in local bileşeninde yönetilmesi(don’t lift state up) mümkünse burada state’in üst bileşenlere taşınması tercih edilmemelidir. React tree’de yukarı kaldırılan state’ler iyi yönetilmediği takdirde tree’in alt katmanında bulunan bileşenlerin gereksiz renderlanmasına sebebiyet verebileceği unutulmamalıdır.
- Renderlama süreçlerinin yan etkilerden uzak olarak yapılması(pure logic) oldukça önemlidir. Re-renderlama süreci problem yaratıyor ya da hissedilir derecede görsel kusurlar ortaya çıkarıyorsa buraki bug’un çözülmesi önceliklendirilmelidir sonrasında optimizasyon yapılabilir.
- State’i gereksiz yere renderlayan Effect’lerden kurtulunmalıdır(unnecessary Effects). Performans problemlerinin çoğu effect’lerden kaynaklı ortaya çıkan güncelleme zincirlerinin gereksizce renderlama meydana getirmesidir. Effect’ler mümkün olduğunca asgari ve kontrollü kullanılmalıdır.
- Effect’leri tetikleyen gereksiz bağlılıklardan kaçınılmalıdır(unnecessary dependencies). Burada özellikle referans bazlı değer tutan nesne ve fonksiyonların Effect’ler içerisinde tekrar tekrar yaratılması istenmeyen durumların ortaya çıkmasına sebep olur.
SONUÇ OLARAK
- Cacheleme mekanizmaları çoğu zaman gereksiz ve kodu daha az okunabilir hale getiren yapılardır. Bir hata ile karşılaşıldığında öncelikle hatanın düzeltilmesi sonrasında gerekli görüldüğü takdirde React özelinde yukarıda tavsiye edilen hook’ların ve API’lerin kullanılması yoluna gidilmelidir.
- Cacheleme mekanizması bütüncül olarak ele alınmalıdır. Bu işlemlerin yapılması gerekli görülen alanların DOM tree içerisinde dikey hiyerarşide re-render’a sebebiyet verecek tek bir değerin olması cachelenen diğer bir çok değere yapılan işlemleri gereksiz kılacaktır.
- Effect’lerin ve bağımlılık listelerinin kontrollü oluşturulması, bileşenlerin hiyerarşik mimarilerinin ve state tanımlarının doğru yerde yapılması, React’in kendi içinde uyguladığı performans optimizasyonun gücünü yanınıza almanızı sağlayacak ve yukarıda tanımlanan yapılara çoğu zaman ihtiyaç dahi duymayacaksınız.
#React #ReactJS #Memoization #YazılımGeliştirme #FrontendDevelopment#CodingTips
KAYNAKLAR: