Takip et

React.memo Yeterli Değil — Kıdemli Geliştiricilerin Kullandığı 4 Performans İyileştirmesi

import React, { useState, useCallback } from ‘react’; const Button = React. memo(({ onClick, children }) => { console.

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';

//

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık