React Performance

Preventing unnecessary re-renders with React.memo, useMemo, and useCallback, and measuring render times with React's Profiler component -- with simple examples.

Advanced 12 min
TR

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.