React Performance
Bu kategori, React'in "ileri seviye" konularına giriş yapıyor -- artık temel kavramları (state, hooks, forms, routing, veri çekme, state paylaşımı) bildiğin için, bunları NASIL DAHA VERİMLİ kullanacağına odaklanabiliriz. İlk durak: gereksiz yeniden render'ları önlemek.
Gereksiz Yeniden Render'lar
Bir parent component yeniden render olduğunda, React varsayılan olarak TÜM child'larını da yeniden render eder -- child'ın props'u değişmemiş olsa bile:
import { useState } from "react";
function ExpensiveList({ items }) {
// Bu component'in her render'ı "pahalı" olsun diye console.log koyduk --
// gerçek bir uygulamada bu, büyük bir liste ya da karmaşık bir hesaplama
// olabilir.
console.log("ExpensiveList render edildi");
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
const items = ["Java", "React", "Spring Boot"];
function UnnecessaryRerenderExample() {
const [count, setCount] = useState(0);
return (
<div>
{/* `count` her arttığında, UnnecessaryRerenderExample yeniden render
olur -- ve React, varsayılan olarak, parent'ı render olan HER
child'ı da yeniden render eder. ExpensiveList'in props'u (`items`)
hiç DEĞİŞMEDİĞİ halde, o da gereksiz yere yeniden render olur. */}
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveList items={items} />
</div>
);
}
count her arttığında, items HİÇ DEĞİŞMEDİĞİ halde ExpensiveList
de yeniden render olur. Küçük component'lerde bu sorun olmaz, ama
büyük listelerde ya da karmaşık hesaplamalarda fark edilir bir yavaşlığa
yol açabilir.
React.memo ile Gereksiz Render'ı Atlamak
memo(), bir component'i, props'u değişmediği sürece yeniden render
ETMEYECEK şekilde sarmalar:
import { memo, useState } from "react";
// memo(), bir component'i "hatırlanan" (memoized) bir versiyona sarmalar --
// React, yeniden render etmeden ÖNCE, props'ların bir öncekiyle AYNI olup
// olmadığını kontrol eder; aynıysa, render'ı ATLAR.
const ExpensiveList = memo(function ExpensiveList({ items }) {
console.log("ExpensiveList render edildi");
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
});
// `items`'ı component'in DIŞINDA tanımlıyoruz -- component'in içine
// koysaydık, her render'da YENİ bir dizi oluşturulur, referansı değişir ve
// memo hiçbir zaman render'ı atlayamazdı (bkz. bir sonraki ders,
// "ReactMemoWithCallbackExample").
const items = ["Java", "React", "Spring Boot"];
function ReactMemoExample() {
const [count, setCount] = useState(0);
return (
<div>
{/* `count` arttığında ReactMemoExample yeniden render olur, ama
`items`'ın İÇERİĞİ aynı kaldığı için (aynı dizi elemanları),
memo artık ExpensiveList'in yeniden render edilmesini ATLAR. */}
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveList items={items} />
</div>
);
}
React, memo ile sarmalanmış bir component'i yeniden render etmeden
ÖNCE, yeni props'ları bir önceki render'daki props'larla KARŞILAŞTIRIR
(sığ/shallow karşılaştırma); aynılarsa render'ı atlar.
memo + useCallback: Fonksiyon Prop'ları
memo tek başına yeterli değil -- fonksiyon prop'larıyla dikkatli
olmak gerekir:
import { memo, useCallback, useState } from "react";
const SearchButton = memo(function SearchButton({ onSearch }) {
console.log("SearchButton render edildi");
return <button onClick={onSearch}>Search</button>;
});
function ReactMemoWithCallbackExample() {
const [count, setCount] = useState(0);
const [query, setQuery] = useState("");
// useCallback OLMADAN, her render'da `handleSearch` YENİ bir fonksiyon
// olurdu -- fonksiyonlar da birer değer olduğu için, memo bunu "props
// değişti" sayar ve SearchButton'ı yine de yeniden render ederdi.
// useCallback, `query` değişmediği sürece AYNI fonksiyon referansını
// korur.
const handleSearch = useCallback(() => {
console.log("Searching for:", query);
}, [query]);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input value={query} onChange={(event) => setQuery(event.target.value)} />
<SearchButton onSearch={handleSearch} />
</div>
);
}
Hooks dersinde gördüğümüz useCallback OLMADAN, her render'da YENİ bir
fonksiyon oluşur -- fonksiyonlar da birer değer olduğu için, memo bunu
"props değişti" sayar ve yine de yeniden render eder. useCallback,
bağımlılıkları değişmediği sürece AYNI fonksiyon referansını korur.
useMemo ile Pahalı Hesaplamaları Önbelleklemek
Hooks dersinde gördüğümüz useMemo'yu, şimdi doğrudan performans
bağlamında tekrar görelim:
import { useMemo, useState } from "react";
const courses = ["Java", "React", "Spring Boot", "PostgreSQL", "Docker"];
function sortAlphabetically(list) {
console.log("sortAlphabetically çalıştı (pahalı işlem)");
return [...list].sort();
}
function UseMemoForExpensiveCalculationExample() {
const [query, setQuery] = useState("");
const [count, setCount] = useState(0);
// `sortAlphabetically`'nin sonucu yalnızca `courses` DEĞİŞTİĞİNDE tekrar
// hesaplanır -- `count` değiştiğinde component yeniden render olsa bile,
// `courses` aynı kaldığı için useMemo ÖNBELLEKTEKİ sonucu döner, sıralama
// fonksiyonunu TEKRAR ÇALIŞTIRMAZ.
const sortedCourses = useMemo(() => sortAlphabetically(courses), [courses]);
const filtered = sortedCourses.filter((course) =>
course.toLowerCase().includes(query.toLowerCase()),
);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input value={query} onChange={(event) => setQuery(event.target.value)} />
<ul>
{filtered.map((course) => (
<li key={course}>{course}</li>
))}
</ul>
</div>
);
}
sortAlphabetically, yalnızca courses DEĞİŞTİĞİNDE çalışır --
component'in başka bir nedenle (örneğin count değiştiği için) yeniden
render olması, bu pahalı işlemi TEKRAR TETİKLEMEZ.
React DevTools Profiler ve <Profiler>
Bir component'in ne kadar sürede render edildiğini ÖLÇMEK için,
React'in kendi <Profiler> component'i kullanılabilir:
import { Profiler, useState } from "react";
function onRenderCallback(id, phase, actualDuration) {
// React, her render'dan sonra bu fonksiyonu çağırır -- `id`, Profiler'a
// verdiğimiz isim; `phase`, "mount" (ilk render) mı yoksa "update" (yeniden
// render) mı olduğu; `actualDuration`, render'ın kaç milisaniye sürdüğü.
console.log(`${id} (${phase}) took ${actualDuration.toFixed(2)}ms`);
}
function CourseList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
function ProfilerComponentExample() {
const [count, setCount] = useState(0);
const items = ["Java", "React", "Spring Boot"];
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* React'in yerleşik <Profiler> component'i, sarmaladığı ağacın render
süresini ÖLÇER -- React DevTools'taki "Profiler" sekmesinin
arkasındaki mekanizma budur. Gerçek uygulamalarda genellikle
kalıcı kod olarak değil, performans sorununu ARAŞTIRIRKEN geçici
olarak eklenir. */}
<Profiler id="CourseList" onRender={onRenderCallback}>
<CourseList items={items} />
</Profiler>
</div>
);
}
onRender callback'i, sarmalanan ağaç her render olduğunda çağrılır ve
render süresini milisaniye cinsinden verir. Bu, React DevTools
tarayıcı eklentisindeki "Profiler" sekmesinin arkasındaki mekanizmayla
aynıdır -- gerçek uygulamalarda genellikle kalıcı kod olarak değil,
bir performans sorununu araştırırken GEÇİCİ olarak eklenir.
Özet ve Terimler Sözlüğü
Bir parent yeniden render olduğunda, React varsayılan olarak tüm
child'larını da render eder; memo(), props değişmediği sürece bunu
atlamayı sağlar -- ama fonksiyon prop'ları için useCallback ile
birlikte kullanılması gerekir. useMemo, pahalı bir hesaplamanın
sonucunu, bağımlılıkları değişmediği sürece önbellekte tutar. Bir
component'in render süresini ölçmek için React'in <Profiler>
component'i ya da React DevTools kullanılabilir.
Terimler Sözlüğü
Re-render (Yeniden Render) — Bir component'in, state ya da props değişikliği sonucunda tekrar render edilmesi.
Memoization (Önbellekleme) — Bir hesaplamanın ya da component render'ının sonucunu saklayıp, girdiler değişmediği sürece tekrar hesaplamak yerine bu saklanan sonucu kullanmak.