React.memo Yeterli Değil — Kıdemli Geliştiricilerin Kullandığı 4 Performans İyileştirmesi
React uygulamalarınızda performans darboğazlarıyla mı karşılaşıyorsunuz? Belki de React.memo‘yu kullandınız ama yine de beklediğiniz etkiyi göremediniz. Bu makale, React.memo‘nun ötesine geçerek kıdemli geliştiricilerin karmaşık React uygulamalarında performansı artırmak için fiilen kullandığı ileri düzey teknikleri ele alıyor. Amacımız, uygulamanızın sadece çalışmasını sağlamak değil, aynı zamanda akıcı ve hızlı bir kullanıcı deneyimi sunmasını garantilemek.
Gereksiz Yeniden Oluşturmaları Engellemek için useCallback ve useMemo‘nun Doğru Kullanımı
React.memo bir bileşenin prop’ları değişmediği sürece yeniden render edilmesini engeller. Ancak, prop’lar arasında fonksiyonlar veya objeler olduğunda, JavaScript’in referans eşitliği prensibi nedeniyle React.memo beklenen etkiyi göstermeyebilir. İşte burada useCallback ve useMemo devreye girer.
Neden Sadece React.memo Yeterli Değil? (Referans Eşitliği)
Her yeniden render döngüsünde, bir React fonksiyonel bileşeni yeniden çalışır. Bu, bileşenin içinde tanımlanan tüm fonksiyonların ve objelerin de yeniden oluşturulması anlamına gelir. Eğer bu yeni oluşturulan fonksiyon veya obje, React.memo ile sarılmış bir alt bileşene prop olarak geçirilirse, prop’lar değişmiş gibi algılanır (çünkü referansları farklıdır) ve alt bileşen gereksiz yere yeniden render edilir.
useCallback ile Fonksiyon Referanslarını Sabitleme
useCallback hook’u, bir fonksiyonun referansının, bağımlılıkları değişmediği sürece aynı kalmasını sağlar. Bu, özellikle alt bileşenlere prop olarak geçen fonksiyonlar için kritiktir.
import React, { useState, useCallback } from 'react';
const Button = React.memo(({ onClick, children }) => {
console.log('Button rendered:', children);
return ;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// count değiştiğinde yeni bir referans oluşmaz
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // Bağımlılık dizisi boş, bu fonksiyon sadece bir kez oluşturulur.
const handleInputChange = useCallback((e) => {
setText(e.target.value);
}, []); // Bağımlılık dizisi boş
return (
Count: {count}
{/* Bu her zaman yeniden render olur */}
);
}
Yukarıdaki örnekte, handleClick fonksiyonu useCallback ile sarıldığı için, ParentComponent her yeniden render olduğunda yeni bir referans oluşturmaz. Bu sayede Button bileşeni, count veya text state'i değiştiğinde gereksiz yere yeniden render olmaz.
useMemo ile Değerlerin Yeniden Hesaplanmasını Önleme
useMemo hook'u, pahalı hesaplamaların sonucunu önbelleğe almak (memoize etmek) için kullanılır. Bağımlılıkları değişmediği sürece, daha önce hesaplanmış değeri döndürür ve hesaplamayı yeniden yapmaz.
import React, { useState, useMemo } from 'react';
function ExpensiveCalculationComponent({ items }) {
console.log('ExpensiveCalculationComponent rendered');
const total = useMemo(() => {
console.log('Pahalı hesaplama yapılıyor...');
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]); // Sadece items prop'u değiştiğinde yeniden hesaplanır.
return (
Toplam Değer: {total}
);
}
function App() {
const [data, setData] = useState([{ id: 1, value: 10 }, { id: 2, value: 20 }]);
const [filter, setFilter] = useState('');
// data veya filter değiştiğinde yeni bir obje oluşturulur.
// Ancak ExpensiveCalculationComponent'e geçilen 'data' prop'u değişmediği sürece
// içindeki useMemo hesaplaması yeniden yapılmaz.
return (
setFilter(e.target.value)} />
);
}
total değeri, sadece items bağımlılığı değiştiğinde yeniden hesaplanır. filter state'i değiştiğinde App bileşeni yeniden render olsa bile, ExpensiveCalculationComponent'a geçen items prop'u aynı kaldığı için total hesaplaması tekrarlanmaz.
Bağımlılık Dizilerine Dikkat: Tuzaklar ve En İyi Uygulamalar
useCallback ve useMemo'nun en yaygın hatası, bağımlılık dizisini yanlış kullanmaktır.
- Boş Bağımlılık Dizisi ([]): Fonksiyon veya değer sadece ilk render'da oluşturulur ve asla değişmez. İçinde kullanılan state veya prop'lar güncel olmayabilir.
- Eksik Bağımlılıklar: Fonksiyon veya değer, bağımlılık dizisinde olmayan bir state veya prop kullandığında eski (stale) değerlere sahip olabilir. Bu, bug'lara yol açar.
- Gereksiz Bağımlılıklar: Bağımlılık dizisine gereksiz yere eklenen değişkenler, fonksiyonun veya değerin daha sık yeniden oluşturulmasına neden olarak memoization'ın faydasını azaltır.
En İyi Uygulama: eslint-plugin-react-hooks eklentisini kullanarak bağımlılık dizisi uyarılarını takip etmek ve daima güncel bağımlılıkları eklemek. Mümkünse, bağımlılıkları azaltmak için fonksiyonları veya state'i daha alt seviyelere taşımak.
Büyük Listelerle Başa Çıkmak: Sanallaştırma (Virtualization)
Binlerce öğeden oluşan listeleri doğrudan DOM'a render etmek, uygulamanın performansını ciddi şekilde düşürebilir. Sayfa kaydırma, DOM manipülasyonları ve tarayıcının layout/paint işlemleri için aşırı yük oluşturur. Sanallaştırma, bu sorunu çözmek için kullanılan güçlü bir tekniktir.
Sanallaştırma Nedir ve Neden Önemlidir?
Sanallaştırma (veya pencereleme - windowing), sadece kullanıcının görünür alanında (viewport) bulunan liste öğelerini DOM'a render etme tekniğidir. Görünür alanın dışındaki öğeler render edilmez veya DOM'dan kaldırılır. Kullanıcı kaydırdıkça, görünür alana giren yeni öğeler render edilir, çıkanlar ise kaldırılır. Bu, DOM boyutunu ve dolayısıyla tarayıcının iş yükünü önemli ölçüde azaltır.
Nasıl Çalışır? Temel Prensipler
1. Görünür Alan Tespiti: Hangi öğelerin şu anda kullanıcının ekranında göründüğü hesaplanır.
2. Sınırlı Render: Sadece bu görünür öğeler ve genellikle bir miktar "tampon" (buffer) öğesi (kaydırma deneyimini pürüzsüz hale getirmek için görünür alanın hemen üstünde ve altında) render edilir.
3. Yer Tutucular: Render edilmeyen öğelerin yerini tutmak için boşluk (padding veya margin) kullanılır, böylece kaydırma çubuğunun boyutu ve davranışı doğru kalır.
4. Dinamik Güncelleme: Kullanıcı kaydırdıkça, görünür alan değişir ve render edilen öğe seti dinamik olarak güncellenir.
Popüler Kütüphaneler ve Kullanım Örnekleri
React ekosisteminde sanallaştırma için iki popüler kütüphane bulunmaktadır:
- react-window: Daha küçük, daha hızlı ve daha esnektir. Sabit boyutlu öğeler için optimize edilmiştir.
- react-virtualized: Daha kapsamlı özellik setine sahiptir ancak daha büyük bir kütüphanedir. Değişken boyutlu öğeler ve daha karmaşık senaryolar için uygundur.
react-window ile Sabit Boyutlu Liste Örneği:
import React from 'react';
import { FixedSizeList } from 'react-window';
const Row = ({ index, style }) => (
Liste Öğesi {index}
);
function VirtualizedList() {
const itemCount = 10000; // 10.000 öğe
const itemSize = 50; // Her öğenin yüksekliği 50px
return (
{Row}
);
}
Bu örnekte, 10.000 öğe olmasına rağmen, DOM'da sadece height ve itemSize'a göre belirlenen sayıda öğe (örneğin, 400/50 = 8 öğe + tampon) bulunur.
Sanallaştırmanın Getirdiği Zorluklar
- Değişken Öğe Boyutları: Öğelerin boyutları dinamik olarak değişiyorsa (örneğin, farklı uzunluklarda metin içeren öğeler), sanallaştırma uygulaması daha karmaşık hale gelir. react-virtualized gibi kütüphaneler bu durumu yönetmek için çözümler sunar.
- SEO: Arama motorları genellikle sadece DOM'da bulunan içeriği indeksler. Sanallaştırılmış listelerdeki tüm içerik DOM'da olmadığı için SEO açısından bazı zorluklar yaşanabilir. Sunucu tarafı render (SSR) veya önceden oluşturma (pre-rendering) bu sorunu hafifletebilir.
- Kaydırma Konumu: Kullanıcının kaydırma konumunu doğru bir şekilde korumak, özellikle öğeler dinamik olarak yüklenip kaldırılırken dikkat gerektirir.
Context API'da Performans Tuzağı ve Çözümleri
React Context API, prop drilling'i önlemek ve global state'i kolayca paylaşmak için harika bir araçtır. Ancak yanlış kullanıldığında, gereksiz yeniden oluşturmalara yol açarak performans sorunlarına neden olabilir.
Context API ve Yeniden Oluşturma Sorunu
Bir Context.Provider'ın değeri değiştiğinde, o Context'i tüketen tüm bileşenler (yani useContext hook'unu kullanan veya Context.Consumer ile sarılmış tüm bileşenler), o Context'in değerinin sadece küçük bir kısmı değişse bile yeniden render edilir. Eğer Context objesi çok büyükse veya sık sık güncelleniyorsa, bu durum ciddi performans darboğazlarına yol açabilir.
// Kötü örnek: Provider'ın değeri değiştiğinde tüm tüketiciler render olur
const MyContext = React.createContext({});
function MyProvider({ children }) {
const [user, setUser] = useState({ name: 'Ali', email: 'ali@example.com', theme: 'dark' });
const [settings, setSettings] = useState({ notifications: true, language: 'tr' });
// Herhangi bir state değiştiğinde bu obje yeniden oluşturulur
const contextValue = { user, settings, setUser, setSettings };
return (
{children}
);
}
function UserNameDisplay() {
const { user } = useContext(MyContext); // Sadece user.name ile ilgileniyor
console.log('UserNameDisplay rendered');
return Kullanıcı Adı: {user.name}
;
}
function ThemeDisplay() {
const { user } = useContext(MyContext); // Sadece user.theme ile ilgileniyor
console.log('ThemeDisplay rendered');
return Tema: {user.theme}
;
}
// ... Diğer bileşenler
Yukarıdaki örnekte, settings state'i değişse bile UserNameDisplay ve ThemeDisplay bileşenleri yeniden render olacaktır, çünkü MyContext.Provider'ın value prop'u yeni bir referansla güncellenmiştir.
Selector Pattern ile Hedefli Güncellemeler
Bu sorunu çözmek için "Selector Pattern" adı verilen bir yaklaşım kullanabiliriz. Temel fikir, Context'i doğrudan tüketmek yerine, sadece ilgilendiğimiz parçayı seçen ve diğer değişikliklerden etkilenmeyen özel hook'lar oluşturmaktır.
// İyi örnek: Selector Pattern
import React, { createContext, useContext, useState, useMemo, useCallback } from 'react';
const GlobalStateContext = createContext(null);
function GlobalStateProvider({ children }) {
const [state, setState] = useState({
user: { name: 'Ali', email: 'ali@example.com', theme: 'dark' },
settings: { notifications: true, language: 'tr' },
});
const updateState = useCallback((key, value) => {
setState(prevState => ({
...prevState,
[key]: { ...prevState[key], ...value }
}));
}, []);
const contextValue = useMemo(() => ({ state, updateState }), [state, updateState]);
return (
{children}
);
}
// Custom hook'lar ile selector pattern
function useUser() {
const { state, updateState } = useContext(GlobalStateContext);
const setUser = useCallback((newUser) => updateState('user', newUser), [updateState]);
return [state.user, setUser];
}
function useSettings() {
const { state, updateState } = useContext(GlobalStateContext);
const setSettings = useCallback((newSettings) => updateState('settings', newSettings), [updateState]);
return [state.settings, setSettings];
}
// Bileşenler artık sadece ilgili parçayı tüketiyor
function UserNameDisplayOptimized() {
const [user] = useUser();
console.log('UserNameDisplayOptimized rendered');
return Kullanıcı Adı: {user.name}
;
}
function ThemeDisplayOptimized() {
const [user, setUser] = useUser();
console.log('ThemeDisplayOptimized rendered');
const toggleTheme = () => setUser({ ...user, theme: user.theme === 'dark' ? 'light' : 'dark' });
return (
Tema: {user.theme}
);
}
function LanguageDisplayOptimized() {
const [settings] = useSettings();
console.log('LanguageDisplayOptimized rendered');
return Dil: {settings.language}
;
}
function AppWithOptimizedContext() {
return (
);
}
Bu yaklaşımda, GlobalStateContext'in value'su değiştiğinde tüm bileşenler render olsa bile, useUser ve useSettings gibi custom hook'lar içindeki useMemo veya useCallback sayesinde, sadece ilgili state parçası değiştiğinde bileşenler yeniden render olur. Daha gelişmiş Context yönetim kütüphaneleri (örneğin zustand, jotai, redux ile react-redux'un useSelector'ı) bu selector mantığını kendi içlerinde barındırır.
Birden Fazla Context Kullanımı
Büyük bir state'i tek bir Context'te toplamak yerine, farklı mantıksal alanlar için birden fazla Context oluşturmak da bir çözümdür. Örneğin, UserContext ve SettingsContext gibi ayrı Context'ler oluşturarak, bir Context'teki değişikliklerin diğer Context'i tüketen bileşenleri etkilemesini önleyebilirsiniz.
const UserContext = createContext(null);
const SettingsContext = createContext(null);
function AppProviders({ children }) {
const [user, setUser] = useState({ name: 'Ali', theme: 'dark' });
const [settings, setSettings] = useState({ notifications: true });
const userValue = useMemo(() => ({ user, setUser }), [user, setUser]);
const settingsValue = useMemo(() => ({ settings, setSettings }), [settings, setSettings]);
return (
{children}
);
}
function UserName() {
const { user } = useContext(UserContext);
console.log('UserName rendered');
return Ad: {user.name}
;
}
function AppWithMultipleContexts() {
return (
{/* SettingsContext'i tüketen bir bileşen */}
);
}
Bu senaryoda, settings değiştiğinde UserName bileşeni yeniden render olmaz, çünkü sadece UserContext'i tüketmektedir ve UserContext'in değeri değişmemiştir.
useReducer ve Context Kombinasyonu
Karmaşık state mantığını yönetmek ve Context performansını optimize etmek için useReducer hook'u Context ile birlikte kullanılabilir. useReducer'ın dispatch fonksiyonu referans olarak stabildir, yani her render'da yeniden oluşturulmaz. Bu, dispatch'i Context aracılığıyla paylaştığınızda, alt bileşenlerin gereksiz yere yeniden render olmasını engeller.
const initialState = { count: 0, text: '' };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'setText':
return { ...state, text: action.payload };
default:
throw new Error();
}
}
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function ReducerProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
{children}
);
}
function CounterDisplay() {
const state = useContext(StateContext);
console.log('CounterDisplay rendered');
return Sayı: {state.count}
;
}
function TextInput() {
const state = useContext(StateContext);
const dispatch = useContext(DispatchContext);
console.log('TextInput rendered');
return (
dispatch({ type: 'setText', payload: e.target.value })}
/>
);
}
Bu yapıda, StateContext'i tüketen bileşenler state değiştiğinde render olurken, DispatchContext'i tüketen bileşenler (sadece dispatch fonksiyonunu kullanıyorlarsa) dispatch'in referansı sabit olduğu için gereksiz yere render olmaz.
Ağır İşlemleri Erteleme ve Sınırlama: Debounce ve Throttle
Kullanıcı etkileşimleri veya DOM olayları (örneğin, klavye girişi, pencere boyutlandırma, kaydırma) bazen çok sık tetiklenebilir. Bu tür sık tetiklenen olaylara yanıt olarak pahalı işlemler yapmak, uygulamanın donmasına veya yavaşlamasına neden olabilir. Debounce ve Throttle bu tür olayları yönetmek için kullanılan iki güçlü tekniktir.
Debounce Nedir ve Ne Zaman Kullanılır?
Debounce, bir fonksiyonun belirli bir süre içinde birden fazla kez çağrılmasını engeller. Fonksiyon, yalnızca belirli bir süre boyunca hiçbir çağrı yapılmadığında çalışır. Örneğin, kullanıcı yazmayı bıraktığında arama işlemi yapmak için idealdir.
Kullanım Senaryoları:
- Arama çubuğuna yazarken API çağrısı yapmak (kullanıcı yazmayı bitirene kadar beklemek).
- Pencere boyutlandırma olaylarına yanıt vermek.
- Otomatik kaydetme işlevleri.
Debounce Uygulama Örneği:
import React, { useState, useEffect, useCallback } from 'react';
import debounce from 'lodash.debounce'; // Veya kendi debounce fonksiyonunuz
function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
// Bu fonksiyon sadece 500ms içinde başka bir çağrı gelmezse çalışır
const debouncedSearch = useCallback(
debounce((query) => {
console.log('API çağrısı yapılıyor:', query);
// Burada API çağrısı yapılabilir
}, 500),
[] // debounce fonksiyonunun kendisinin referansının değişmemesi için
);
const handleChange = (e) => {
const value = e.target.value;
setSearchTerm(value);
debouncedSearch(value); // Her tuş vuruşunda çağrılır, ancak debounce bekler
};
useEffect(() => {
return () => {
debouncedSearch.cancel(); // Bileşen unmount edildiğinde bekleyen çağrıyı iptal et
};
}, [debouncedSearch]);
return (
);
}
Yukarıdaki örnekte, kullanıcı her tuşa bastığında debouncedSearch çağrılır, ancak console.log (veya API çağrısı) sadece 500 milisaniye boyunca başka bir tuş vuruşu olmazsa tetiklenir.
Throttle Nedir ve Ne Zaman Kullanılır?
Throttle, bir fonksiyonun belirli bir zaman aralığında en fazla bir kez çalışmasını sağlar. Fonksiyon, çağrılar ne kadar sık gelirse gelsin, belirlenen aralıkta sadece bir kez tetiklenir. Örneğin, kaydırma olaylarını izlemek için idealdir.
Kullanım Senaryoları:
- Kaydırma sırasında bir animasyonu güncellemek.
- Oyunlardaki ateş etme tuşu gibi hızlı tıklamaları sınırlamak.
- Sürükle-bırak işlemleri sırasında konum güncellemeleri.
Throttle Uygulama Örneği:
import React, { useState, useEffect, useCallback } from 'react';
import throttle from 'lodash.throttle'; // Veya kendi throttle fonksiyonunuz
function ScrollTracker() {
const [scrollPosition, setScrollPosition] = useState(0);
// Bu fonksiyon sadece 200ms'de bir çalışır
const handleScroll = useCallback(
throttle(() => {
const currentPosition = window.scrollY;
console.log('Kaydırma konumu güncellendi:', currentPosition);
setScrollPosition(currentPosition);
}, 200),
[]
);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
handleScroll.cancel(); // Bileşen unmount edildiğinde bekleyen çağrıyı iptal et
};
}, [handleScroll]);
return (
Kaydırma Konumu: {scrollPosition}px
Aşağı kaydırın...
);
}
Bu örnekte, kullanıcı kaydırdığında handleScroll fonksiyonu sıkça tetiklenir, ancak setScrollPosition (ve console.log) sadece her 200 milisaniyede bir güncellenir.
Hangi Durumda Hangisini Seçmeli?
| Özellik | Debounce | Throttle |
| :--------------- | :------------------------------------- | :--------------------------------------- |
| Çalışma Prensibi | Olaylar durduğunda çalışır. | Belirli aralıklarla çalışır. |
| Ne Zaman Tetiklenir | En son olaydan sonra delay kadar. | İlk olayda ve sonra her delay aralığında. |
| Kullanım Alanı | Kullanıcının "bitirdiğini" beklemek. | Sürekli olayları düzenli aralıklarla işlemek. |
| Örnekler | Arama çubuğu, pencere boyutlandırma. | Kaydırma, sürükleme, oyunlardaki aksiyonlar. |
Özetle, bir işlemin yalnızca bir dizi olayın sonunda gerçekleşmesini istiyorsanız debounce kullanın. Bir işlemin düzenli aralıklarla gerçekleşmesini, ancak çok sık olmamasını istiyorsanız throttle kullanın.
Uygulama Boyutunu Küçültmek: Kod Bölümleme (Code Splitting) ve Lazy Loading
Büyük React uygulamaları, başlangıçta kullanıcıya gönderilen JavaScript dosyasının boyutunu artırabilir. Bu, uygulamanın yüklenme süresini uzatır ve özellikle mobil cihazlarda veya yavaş internet bağlantılarında kötü bir kullanıcı deneyimine yol açar. Kod bölümleme ve lazy loading, bu sorunu çözmek için kullanılan tekniklerdir.
Neden Kod Bölümleme Yapmalıyız?
Kod bölümleme (Code Splitting), uygulamanızın kodunu daha küçük parçalara (chunk'lara) bölme işlemidir. Bu parçalar, uygulamanın tamamı yerine yalnızca ihtiyaç duyulduğunda yüklenir. Bu, ilk yükleme süresini önemli ölçüde azaltır, çünkü tarayıcının indirmesi ve ayrıştırması gereken JavaScript miktarı azalır.
Faydaları:
- Daha Hızlı İlk Yükleme: Kullanıcılar uygulamanızı daha çabuk görmeye başlar.
- Daha İyi Performans: Daha az JavaScript, tarayıcının daha az iş yapması anlamına gelir.
- Daha Verimli Kaynak Kullanımı: Sadece ihtiyaç duyulan kod indirilir.
React.lazy ve Suspense ile Temel Kullanım
React, kod bölümlemeyi kolaylaştırmak için React.lazy ve React.Suspense'i sağlar.
- React.lazy: Dinamik bir import kullanarak bir bileşeni tembelce (lazy) yüklemenizi sağlar.
- React.Suspense: Tembelce yüklenen bir bileşen yüklenirken bir geri dönüş (fallback) içeriği göstermenizi sağlar.
```javascript
import React, { Suspense } from 'react';
//
