React Performance

React.memo, useMemo ve useCallback ile gereksiz yeniden render'ları önlemek, ve React'in Profiler component'i ile render sürelerini ölçmek -- basit örneklerle.

İleri 12 dk
EN

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.