React Performance
This category is our introduction to React's "advanced" topics -- now that you know the fundamentals (state, hooks, forms, routing, fetching data, sharing state), we can focus on using them MORE EFFICIENTLY. First stop: preventing unnecessary re-renders.
Unnecessary Re-renders
When a parent component re-renders, React by default re-renders ALL of its children too -- even if the child's props haven't changed:
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>
);
}
Every time count increases, ExpensiveList re-renders even though
items NEVER changed. This isn't a problem for small components, but
in large lists or complex calculations it can cause noticeable
slowdowns.
Skipping Unnecessary Renders with React.memo
memo() wraps a component so it will NOT re-render unless its props
change:
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>
);
}
Before re-rendering a memo-wrapped component, React COMPARES the new
props with the props from the last render (a shallow comparison); if
they're the same, it skips the render.
memo + useCallback: Function Props
memo alone isn't enough -- you need to be careful with function
props:
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>
);
}
WITHOUT the useCallback we saw in the Hooks lesson, a NEW function is
created on every render -- since functions are values too, memo
treats this as "the props changed" and re-renders anyway. useCallback
keeps the SAME function reference as long as its dependencies haven't
changed.
Caching Expensive Calculations with useMemo
Let's revisit the useMemo we saw in Hooks, this time directly in a
performance context:
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 only runs when courses CHANGES -- the component
re-rendering for some other reason (like count changing) does NOT
re-trigger this expensive operation.
React DevTools Profiler and <Profiler>
To MEASURE how long a component takes to render, React's own
<Profiler> component can be used:
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>
);
}
The onRender callback is called every time the wrapped tree renders,
giving you the render duration in milliseconds. This is the same
mechanism behind the "Profiler" tab in the React DevTools browser
extension -- in real applications it's usually added TEMPORARILY while
investigating a performance issue, not left in as permanent code.
Summary and Glossary
When a parent re-renders, React by default renders all of its children
too; memo() lets you skip this as long as props haven't changed --
but it needs to be paired with useCallback for function props.
useMemo caches the result of an expensive calculation as long as its
dependencies haven't changed. React's <Profiler> component or React
DevTools can be used to measure a component's render time.
Glossary
Re-render — A component being rendered again as a result of a state or props change.
Memoization — Storing the result of a calculation or component render and reusing that stored result instead of recomputing it, as long as the inputs haven't changed.