React Performans Optimizasyonu ve Memorization(useCallBack,useMemo,Memo,Virtualization)
Merhaba arkadaşlar bugün sizlerle beraber react kullanarak uygulamamızda optimizasyonu nasıl sağlarsınız onu inceleyeceğiz.
Genel react bilginizin olduğunu düşünerek bu içeriği oluşturduğumu bildirmek isterim.
Öncelikle uygulama hakkında kaynak kodlarımız üzerinden konuşalım.Daha hızlı bir şekilde konumuza odaklanmamız için sizlere bazı kodlar vereceğim bu kodları kopyala yapıştr yaparak .
Ben NextJS üzerinden bu işlemi gerçekleştireceğim siz isterseniz pureReact isterseniz Vite üzerinden işlemlerinizi gerçekleştirebilirsiniz.
https://github.com/kubilaybzk/react-memorization
Reposu üzerinden kodları takip edebilirsiniz bu yazıda değindiğim her konu için farklı farklı commitler attım.
Page.js dosyamız
BlogForm.js componentimiz.
BlogItem.js componentimiz.
PostList.js componentimiz.
Header.js componentimiz.
https://gist.github.com/kubilaybzk/216741d772adb12b9eb8043f67ecb35f#file-header-js
Tüm bunları yaptıktan sonra uygulamamızda dosyalarımız ekteki gibi olmalı.
Şimdi tüm bu düzenlemeleri yaptıktan sonra , npm run dev diyerek uygulamamızı başlatalım ve incelemeye başlayalım.
İlk olarak , geliştirici konsol üzerinden hangi alanlar uygulamamıza ilk request atıldığında kullanıcının tarayıcısında render ediliyor ve hangi alanlar uygulamada bulunan herhangi bir state değişiminde render ediliyor inceleyelim.
Şimdi uygulamamız ilk defa tarayıcıya yüklendiği zaman ,
Gördüğünüz gibi ilk olarak bütün componentler render edildi.
Bu gayet normal bir durum değil mi ? Fakat BlogItem render edilmedi çünkü uygulamamız ilk başladığında BlogItem içine gönderdiğimiz state boş. React burada ekstra bir render işlemi gerçekleştirmiyor.
Şimdi uygulamamızda bulunan From içine bir kaç değer girelim .
Gördüğünüz gibi hala herhangi bir render işlemi yok.
Bunun sebebi input değerlerini usestate kullanmak yerine ref olarak almamız, eğer input alanında onChange fonksiyonu içinde her bir değişim için state güncellemesi yapmış olsaydık burada her tuşa bastığımızda input alanı yeniden render olacaktı.
Şimdi butonumuza tıklayalım ve ne olacak görelim.
Gördüğünüz gibi tek bir state güncellememize rağmen , Header, Form ,HomePage tekrardan render edildi.
Peki bir alan daha ekleyelim örnek olarak 2 yazalım inputların içine ;
Gördüğünüz gibi burada ise şöyle bir durum söz konusu oldu ,
2 PostList içine eklendi evet ama yinede Header , Form , HomePage alanları yeniden render edildi ek olarak daha önce eklemiş olduğumuz ve Liste alanında hali hazırda olan ve içinde 1 yazan alanda render edildi.
İşte burada çoook büyük bir liste olduğunu düşünün mesela 1'de başlayarak son değer 1.000.000 olacak şekilde veri eklemiş olalım bu listeye her bir eklemede bu alanların tekrar tekrar render olması performans açısından ne kadar dezavantajlı bir yöntem olacak düşünebiliyor musunuz?
İşte React bu durumlarda kullanmamız için bizlere Memo,UseMemo ve UseCallBack olacak 3 adet performans optimizasyonu için kullanabileceğimiz Hooklar sağlamakta.
Şimdi ilk olarak Memo ile başlayalım.
React Memo
React'te bir bileşenin performansını artırmak amacıyla kullanılan bir yüksek seviye bileşen (higher-order component) veya yüksek seviye fonksiyon (higher-order function) olarak bilinir. Bu işlev, bir bileşeni hatırlayarak ve gereksiz yeniden render işlemlerini önleyerek performansı artırır.
Normalde, bir bileşen herhangi bir değişiklik olduğunda tekrar render edilir. Ancak bazen, bir bileşenin prop’ları veya durumu değişmese bile yeniden render edilmesi gerekebilir. Bu, özellikle büyük ve karmaşık bileşenlerde performans sorunlarına neden olabilir.
Mesela bir Component kendisi render edildiğinde içinde bulunan child component’in state’i değişmemesi durumunda bile kendisi yeniden render edildiği için child componentleri render edilebilir.
React.memo
ile bir bileşen sadece belirli koşullar altında yeniden render edilir. İlgili bileşenin prop'ları veya durumu değiştiğinde sadece o zaman yeniden render edilir. Bu, gereksiz yeniden render işlemlerini önleyerek uygulamanın daha verimli çalışmasını sağlar.
Örnek kullanımı şu şekildedir:
Biz yukarıda listemize 1 ve 2 değerlerini eklemiş olduğumuz iki farklı durum için konsol üzerinde hangi component’in render edildiğini console.log(…) sayesinde görüyoruz , burada her iki durum içinde Header alanının render edildiğini fark ettiniz mi ?
Peki,Header alanında herhangi bir değişiklik söz konusu olmadığı halde neden durmadan render işlemi gerçekleşiyor.
Bunun sebebi aslında çok basit , Page.js dosyamızı incelediğimizde burada bizim bir state değerimiz olduğunu görüyoruz.Bu state her değiştiğinde react bu state’in içinde bulunan her bir component için tekrar bir render işlemi gerçekleştiriyor çünkü parent olan component render ediliyor.
İşte burada Header’ın tekrar tekrar render edilmesi aslında istenmeyen ve performans açısından pek doğru olmayan bir yöntem.
Burada her state değişiminde header alanının tekrar tekrar render olmasını engellemek için burada biz MEMO kullanacağız.
Kullanımı oldukça basit. Memo ile memorize etmek istediğimiz hedef componentimizi parametre olarak ayarlıyoruz.
Şimdi uygulamamızı resfreh edelim ve bir adet listeye eleman ekleme işlemi gerçekleştirelim.
Görüldüğü gibi state değişmesine rağmen header tekrar render edilmedi bu sayede uygulamamızda bir performans artışı yaşamış olduk.
Ek olarak bu memo yapısını uygulamamızda PostList.js içinde kullanabiliriz.Burada nasıl bir problemimiz vardı Liste içine eklenen her bir eleman için React daha önce eklenmiş olan bütün elemanlarıda tekrar tekrar render ediyordu.
Görsel üzerinden hatırlamamız gerekirse;
Şimdi burada biz her bir elemanı farklı bir component olacak şekilde ayarlamıştık yani PostList tüm liste , BlogItem ise liste içinde bulunan tek bir component.
Burada listeyi memo içine sarmalamak şuan istediğimiz durum için bizim hiçbir işimize yaramayacak (sırayla gidiyoruz) , biz liste içinde bulunan elemanları memo kullanarak Memorize etmemiz bizim için istediğimiz optimizasyonu sağlayacak.
Daha anlaşılır bir şekilde açıklamak istiyorum durumu liste içinde 2 farklı Post eklemiş olalım. 3. Post eklendiği anda konsol üzerinde neler oluyor inceleyelim.
Render olan her bir component burada gözülüyor peki title:1 ve title:2 olan değerler neden render ediliyor ? Bunu istemiyoruz hemen memo kullanarak bu problemi çözelim.
Burada başka bir memo kullanım şeklini ele aldık fonksiyon’un tamamını verip kod karmaşası yaratacağımızı export işleminde memo tanımlaması yaptık.
Şimdi 4. post durumunu ekleyelim ve neler olup bitiyor bir göz atalım.
Gördüğünüz gibi sadece o an eklenen eleman render edildi daha önce eklenmiş olan 1,2 ve 3 post componentleri render edilmedi.
useCallback nedir ?
useCallback
, React'te bir fonksiyonun hatırlanmasını (memoization) sağlayan bir hook'tur. Bu hook, belirli bağlamlarda kullanılan fonksiyonların gereksiz yere her render sırasında yeniden oluşturulmasını önlemek için kullanılır. Bu, performansı artırabilir çünkü her render sırasında yeni bir fonksiyon oluşturmak yerine aynı fonksiyonun referansını kullanır.
Genellikle, useCallback
ile oluşturulan fonksiyonlar, başka bileşenlere prop olarak geçirildiğinde veya bağlamalar (context) içinde kullanıldığında faydalıdır. Bu, bu fonksiyonların gereksiz yere her render sırasında yeniden oluşturulmasını önler ve böylece gereksiz işlemci kaynaklarının tüketilmesini engeller.
useCallback
işlevi iki argüman alır:
- Birincisi, hatırlanmasını istediğiniz fonksiyonun kendisidir.
- İkincisi, bağlamasını (dependency array) oluşturmak istediğiniz değişkenlerin bir dizisidir. Eğer bu değişkenler değişirse, hatırlanan fonksiyon yeniden oluşturulur. Eğer bu diziyi boş bırakırsanız, fonksiyon yalnızca bileşenin ilk renderında oluşturulur ve daha sonra herhangi bir değişiklikte yeniden oluşturulmaz.
Basit bir açıklama gerekirse parent component içinde tanımlanan bir fonksiyonu child component’e yolluyorsanız, parent her render edildiğinde child render edilmesin istiyorsanız kullanmanız gereken Hook.
İhtiyaçları, Fonksiyon ve dependency array.
Şimdi teknik bilgileri verdikten hemen sonra,
Uygulamamızda ne olduğunu anlamak için ufak bir modefiye işlemi gerçekleştirelim.
BlogForm içinde bulunan onSavePostClicked fonksiyonunu app.js içine taşıyalım.Daha sonra props olarak bu fonksiyonu BlogForm componentine gönderelim.Daha sonra yeni bir state tanımlayalım herkesin bildiği basit bir count state’i olsun bu. App.js dosyamız ;
Yukarıda buton’a her tıkladığımızda BlogFrom’un render edildiğini görüyorsunuz.
Şimdi BlogFrom’u memo hookunu kullanarak export edelim.
Tekrar uygulamamızda counter değeriyle oynayalım ve sonuca bir göz atalım,
Gördüğünüz gibi memo kullanmamıza rağmen burada Form durmadan render ediliyor bunun sebebi memory içinde bu fonksiyonun tutulmaması her render edildiğinde bu fonksiyonun yeniden ve yeniden memory içinde oluşması.
İşte burada bu durumu engellemek için biz props olarak geçtiğimiz fonksiyonları useCallBack içinde kullanırsak burada ilgili fonksiyonu memory içinde tutmuş oluruz bu sayede BlogForm alanının durmadan render edilmesini engellemiş oluruz.
Gördüğünüz gibi her butona tıkladığımızda BlogForm tekrar tekrar render ediliyor işte burada usememo işe yaramıyor aslında işe yarıyor tek sıkıntı Parent component olan page.js’in içinde olan fonksiyon her çağrıldığında fonksiyonun memory üzerinde değişimleri oluyor.
İşte bu gibi durumlarda usecallback kullanmamız gerekiyor.Tek yapmamız gereken UseCallBack Hook’unun içine bizim kullandığımız onSavePostClicked fonksiyonunun parametre olarak verilmesi.
Dependency Array kısmında ise hangi değer değişirse bunun çalışmasını istiyoruz bunu bildirmemiz gerekiyor.
const onSavePostClicked = useCallback((title, content) => {
setBlogList((prevBlogList) => [
...prevBlogList,
{
title,
content,
},
]);
}, []);
Şimdilik dependency kısmını boş bırakıyoruz artık sadece sayfa yüklendiğinden 1 kere çalışacak eğer biz blogList state’i her değiştiğinde bu fonksiyonun çalışmasını istersek burada dep. array kısmına bloglist state’ini
const onSavePostClicked = useCallback((title, content) => {
setBlogList((prevBlogList) => [
...prevBlogList,
{
title,
content,
},
]);
}, [BlogList]);
Burada başka bir örnek daha vermek istiyorum çünkü burada mantığını anlatmak biraz karışık ve anlaşılması başlangıç aşamasında biraz zormuş gibi geldi bana .
Basit bir counter uygulaması gerçekleştirelim.Ben uygulamada yeni bir route oluşturarak example 2 isminde başka bir app.js dosyası içinde aşağıda bulunan kodları oluşturdum. Öncelikle kodları sizinle paylaşayım,
"use client";
import React, { memo, useCallback, useState } from "react";
export default function CallBackExample() {
console.log("App Render");
const [count, setCount] = useState(0);
const [texy, setText] = useState();
//Arttırma fonksiyonumuz
const increment = () => {
setCount(count + 1);
};
return (
<div className="w-full h-full flex gap-4 justify-center items-center align-middle flex-col">
<div>{count}</div>
<Counter increment={increment} />
<input className="border" type="Text" onChange={(e) => setText(e.target.value)} />
</div>
);
}
const Counter = memo(({ increment }) => {
console.log("Counter Render");
return (
<>
<button onClick={() => increment()}>+</button>
</>
);
});
Uygulamamız çok basit bir counter uygulaması şimdi burada biz useMemo ile counter componentini memory içinde saklayacak şekile getirdik.
Ama burada içeriye biz increment fonksiyonunu yolladık burada beklentimizin konsol üzerinden Counter Rendered değerini görmemek olmaması gerekiyor burada biz bir fonksiyon çağırıyoruz bundan dolayı render oluyor ve her render işleminde bu fonksiyonun memory üzerinde tutulduğu yer değişiyor. useCallback kullanarak fonskiyonumuzu yeniden design edelim ve dependency kısmını boş bıraklım.
"use client";
import React, { memo, useCallback, useState } from "react";
export default function CallBackExample() {
console.log("App Render");
const [count, setCount] = useState(0);
const [texy, setText] = useState();
//Arttırma fonksiyonumuz
const increment = useCallback(() => {
setCount(count + 1);
}, []);
return (
<div className="w-full h-full flex gap-4 justify-center items-center align-middle flex-col">
<div>{count}</div>
<Counter increment={increment} />
<input
className="border"
type="Text"
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
const Counter = memo(({ increment }) => {
console.log("Counter Render");
return (
<>
<button onClick={(e) => increment()}>+</button>
</>
);
});
Şimdi konsol üzerinde uygulamamızı inceleyelim ve neler olduğunu inceleyelim.
Gördüğünüz gibi ne kadar çok tıklarsam tıklayayım tek bir kere çalışıyor bunun sebebi react’in bu fonksiyonu ne zaman çalıştırması gerek bilmiyor olması, burada dependency kısmına biz counter değerini verirsek bu değer her değiştiğinde bunu çalıştırması gerektiğini bildirmiş oluruz.
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
Artık butona her bastığımızda counter değeri artacak ve bu fonksiyon state her değiştiğinde yeniden ve yeniden memory içinde atanacak.
UseMemo nedir ?
useMemo
, React'te hesaplama işlemleri sonucunda oluşturulan değerleri hatırlamanıza (memoization) ve gereksiz hesaplamaların önüne geçmenize yardımcı olan bir hook'tur. Bu hook, belirli değerlerin hesaplamalarının gereksiz yere her render sırasında tekrar tekrar yapılmamasını sağlar, bu da uygulamanızın performansını artırabilir.
useMemo
işlevi iki argüman alır:
- İlk argüman, hesaplamayı yapacak olan fonksiyondur.
- İkinci argüman ise bu hesaplamanın ne zaman yeniden hesaplanması gerektiğini belirleyen bir bağlama dizisidir (dependency array). Eğer bağlama dizisi içindeki değişkenler değişirse, hesaplama işlemi tekrar yapılır. Eğer bağlama dizisi boşsa, hesaplama yalnızca bileşenin ilk render’ında yapılır ve daha sonra herhangi bir değişiklikte tekrarlanmaz.
Yani basit bir şekilde anlatmak gerekirse useMemo ile fonksiyonlardan dönen değerler memoized edilir. Genellikle expensive dediğimiz durumlarda kullanılır.
Bu konu hakkında harika bir örnek paylaşan Berkant Kaya’nın örneğinden faydalanalım.
App klasörü içinde bir adet klasör oluşturalım ve app.js içine ekteki kodu yazalım.
"use client"
import React, { useState } from 'react'
export default function page() {
//Bir adet count verilerini tutan state olsun.
const [count, setCount] = useState(0);
const [text, setText] = useState("");
//Memory üzerinde çok fazla işlem yapan bir fonksiyon
const largeFunction=()=>{
[...new Array(100000000)].forEach((item)=>{})
return count*3
}
const result =largeFunction();
return (
<>
<div>{count}</div>
<button onClick={()=>setCount(count+1)}>
Arttır
</button>
{result}
<input className='border' onChange={(e)=>setText(e.target.value)}/>
</>
)
}
Bu sayfayı ilk açıldığı zamana ne kadar yavaş yükleniyor yenile butonundan görebiliyorsunuz değil mi :)
şimdi burada söyle bir işlem yapacağız bu fonksiyonun bir kere sayfa yüklenirken yada count sayısı değiştiği zaman çalışmasını sağlayacağız çalışma sonucunda elde edilen veri (sonuç) ondan sonra yapılan hiçbir işlemde bu fonksiyon tekrar çalışmayacak.
Senaryamuz şu şekilde olacak butona bastığımız zaman bu fonksiyon yeniden hesaplanacak ve elde edilen veriyi memorize edecek.
Onun dışında sayfada bulunan state değerlerinden etkilenmeyecek bu sayede yeniden hesaplama yapmayacak.
Bunun için UseMemo kullanacağız yapı olarak usecallback ile tamamen aynı yapıya sahip bu hook.
const largeFunction=useMemo(()=>{
[...new Array(100000000)].forEach((item)=>{})
return count*3
},[count])
yapılan işlem bu kadar basit aslında şimdi 2 durum içinde bu geliştirdiğimiz component içinde neler oluyor inceleyelim.
İlk durum sadece input alanı içine bilgiler girelim ve text state’inde manipülasyon uygulayalım.
Şimdi bir diğer durum olarak arttır butonuna basalım.
Gördüğünüz gibi ne kadar uzun sürüyor tekrar hesaplama işlemi .
Şimdi kısa bir özet geçmek gerekirse;
useCallback hooku → geriye memoized bir fonksiyon ddöndürür.
useMemo hooku → useCallback ile çok benzer olup tek farkı geriye memoized bir fonksiyon değil o fonksiyonun döndürdüğü değerleri döndürür.
Memo hooku → İlgili bileşenin prop’ları veya durumu değiştiğinde sadece o zaman yeniden render edilmesini sağlar.
Virtualization
React Virtualization’in temel amacı, büyük veri listelerini sayfada görünür öğelerle sınırlamak ve yalnızca kullanıcının görüntülediği kısmı renderlemektir. Bu, performans artışı , daha az bellek kullanımı ve daha hızlı yanıt gibi avantajlara sebep olur. Yöntemin çalışma mantığını şöyle açıklayayım , doom üzerinde 100000 satır bir tablo oluşturduğunuzu ya da 100000 satır ürün listelediğinizi gerektiğini düşünün .
Sayfa ne kadar yavaş yüklenir ve kullanıcının tarayıcısı üzerinde yaşanacak olan donmaları ve yavaşlamaları düşünebiliyor musunuz?
Daha akılda kalıcı bir örnek vermemiz gerekirse , twitter üzerinden örnek verebiliriz bu konuyu twitter’da ana sayfadan aşağıya doğru inmeye başlayınca ne kadar fazla tweet görüntülersek görüntüleyelim hiçbir donma kasma yaşamıyoruz bunun sebebini söyle inceleyelim.
Geliştirici konsol üzerinden tweetlerin render edildiği html kodları incelendiğinde ne olursa olsun 5 tane verinin bize gösterildiği geri kalan verilerin ekranda gözükmediğini fark ettiniz mi .
İşte burada bahsettiğimiz teknoloji bu işe yarıyor.
Peki bu teknoloji nerelerde kullanılır ?
- Listeler için Infinite Scrolling: Liste benzeri bileşenlerde sonsuz kaydırma (infinite scrolling) kullanarak, sayfa içinde daha fazla öğeyi dinamik olarak yükler. Bu, kullanıcılar listenin sonuna yaklaştıkça daha fazla verinin yüklenmesini sağlar.
- Windowing (Pencereleme): Büyük veri listelerini bir pencere içinde sınırlamak, yalnızca pencere içindeki öğeleri görüntülemeyi ve diğerlerini gizlemeyi içerir. Bu, büyük veri listelerini etkili bir şekilde işlemenin bir yoludur.
- Virtualized List veya Grid Componentleri: React Virtualization için özel olarak tasarlanmış bileşenler, büyük veri listelerini veya tabloları verimli bir şekilde işlemek için kullanılabilir.
Ufak bir virtualiz kütüphanesi kullanarak bir örnek üzerinde inceleyelim.
npm install react-virtualized
import React from 'react';
import { List } from 'react-virtualized';
// Büyük bir liste verisi oluşturalım
const data = Array.from({ length: 1000 }, (_, index) => `Öğe ${index + 1}`);
function VirtualizedList() {
return (
<div>
<h1>Büyük Liste</h1>
<List
width={300} // Liste genişliği
height={400} // Liste yüksekliği
rowCount={data.length} // Öğe sayısı
rowHeight={30} // Her öğenin yüksekliği
rowRenderer={({ index, key, style }) => (
<div key={key} style={style}>
{data[index]}
</div>
)}
/>
</div>
);
}
export default VirtualizedList;
Bu örnek, 1000 öğeyi içeren bir büyük liste görselleştirir. List componenti, react-virtualized
kütüphanesinin bir parçasıdır ve yalnızca görünen öğeleri renderler. rowRenderer Ekranda oluşacak olan component için gerekli olan düzenlemeleri alır.
Elimden geldiğince performans optimizasyonu ve hooklar üzerinden konuyu anlatmaya çalıştım umarım anlaşılır ve güzel bir yazı olmuştur.
Kaynaklar :