İçeriğe geç
react

React Performance Profiling: DevTools ile Analiz

Tarık Tunç
React Performance Profiling: DevTools ile Analiz

React Performance Profiling: DevTools ile Analiz

React uygulamalarında performans sorunları genellikle gereksiz re-render'lar, büyük component tree'ler ve optimize edilmemiş state yönetiminden kaynaklanır. React DevTools Profiler, bu sorunları tespit etmek için güçlü bir araçtır. Bu rehberde React Profiler'ı etkili kullanmayı, yaygın performans sorunlarını teşhis etmeyi ve React.memo, useMemo, useCallback gibi optimizasyon tekniklerini doğru senaryolarda uygulamayı öğreneceksiniz.

React DevTools Profiler Kullanımı

React DevTools Profiler, her render döngüsünde hangi bileşenlerin render edildiğini, ne kadar sürdüğünü ve neden tetiklendiğini gösterir:

  1. React DevTools tarayıcı eklentisini kurun
  2. DevTools'ta "Profiler" sekmesine geçin
  3. Ayarlardan "Record why each component rendered" seçeneğini aktifleyin
  4. Mavi kayıt butonuna basın ve uygulamada etkileşim yapın
  5. Kaydı durdurup flame chart'ı inceleyin
İpucu: Profiler'da "Highlight updates when components render" seçeneğini aktifleştirin. Bu, hangi bileşenlerin ne zaman render edildiğini görsel olarak gösterir. Çok sık yanıp sönen bileşenler, performans sorunlarının kaynağı olabilir.

Programatik Profiling

React'in yerleşik Profiler bileşeni ile render sürelerini programatik olarak ölçebilirsiniz:

import { Profiler, useState, useMemo } from 'react';

function onRenderCallback(
  id,           // Profiler tree'sinin id'si
  phase,        // "mount" veya "update"
  actualDuration,   // Bu render'ın süresi (ms)
  baseDuration,     // Memo olmadan tahmini süre
  startTime,        // Render başlangıç zamanı
  commitTime        // Commit zamanı
) {
  // Yavaş render'ları logla
  if (actualDuration > 16) {  // 16ms = 60fps frame budget
    console.warn(`Yavaş render: ${id}`, {
      phase,
      duration: `${actualDuration.toFixed(2)}ms`,
      baseDuration: `${baseDuration.toFixed(2)}ms`
    });

    // Monitoring servisine gönder
    sendToMonitoring({
      component: id,
      duration: actualDuration,
      phase,
      timestamp: commitTime
    });
  }
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Profiler id="Header" onRender={onRenderCallback}>
        <Header />
      </Profiler>
      <Profiler id="ProductList" onRender={onRenderCallback}>
        <ProductList />
      </Profiler>
    </Profiler>
  );
}

Gereksiz Re-render'ları Tespit Etme ve Önleme

React'te en yaygın performans sorunu gereksiz re-render'lardır. Bir parent bileşen render olduğunda tüm child bileşenler de render olur:

// SORUN: ExpensiveList, count her değiştiğinde gereksiz yere render olur
function App() {
  const [count, setCount] = useState(0);
  const [items] = useState(generateLargeList());

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveList items={items} />  {/* Her count değişiminde render! */}
    </div>
  );
}

// ÇÖZÜM 1: React.memo ile child bileşeni sarma
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');  // Sadece items değiştiğinde
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

// ÇÖZÜM 2: Composition pattern ile state'i izole etme
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

function App() {
  const [items] = useState(generateLargeList());
  return (
    <div>
      <Counter />  {/* State burada izole */}
      <ExpensiveList items={items} />  {/* Etkilenmez */}
    </div>
  );
}
Uyarı: React.memo'yu her yerde kullanmayın. Memo'nun kendisi de maliyet taşır (prop karşılaştırma). Sadece gerçekten yavaş render eden veya sık gereksiz render olan bileşenlerde kullanın. Profiler ile önce sorunu doğrulayın, sonra optimize edin.

useMemo ve useCallback Doğru Kullanımı

Bu hook'lar referans kararlılığı sağlayarak gereksiz hesaplama ve re-render'ları önler:

function ProductPage({ products, category }) {
  // useMemo: Pahalı hesaplamayı cache'le
  // Sadece products veya category değiştiğinde yeniden hesapla
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');  // Ne sıklıkta çağrıldığını izle
    return products
      .filter(p => p.category === category)
      .sort((a, b) => b.rating - a.rating);
  }, [products, category]);

  // useCallback: Fonksiyon referansını kararlı tut
  // React.memo'lu child bileşene prop olarak geçirirken gerekli
  const handleAddToCart = useCallback((productId) => {
    addToCart(productId);
    showNotification('Ürün sepete eklendi');
  }, []);  // Dış bağımlılık yoksa boş array

  return (
    <div>
      <h2>{category} ({filteredProducts.length} ürün)</h2>
      {filteredProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart}  // Kararlı referans
        />
      ))}
    </div>
  );
}

// React.memo ile useCallback birlikte etkili
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>{product.price} TL</p>
      <button onClick={() => onAddToCart(product.id)}>Sepete Ekle</button>
    </div>
  );
});

Virtualization ile Büyük Listeler

Binlerce öğe içeren listelerde virtualization kullanarak sadece görünen öğeleri render edin:

// react-window ile virtualized list
import { FixedSizeList as List } from 'react-window';

function VirtualizedProductList({ products }) {
  const Row = ({ index, style }) => (
    <div style={style} className="product-row">
      <span>{products[index].name}</span>
      <span>{products[index].price} TL</span>
    </div>
  );

  return (
    <List
      height={600}         // Container yüksekliği
      itemCount={products.length}
      itemSize={50}        // Her satır yüksekliği
      width="100%"
      overscanCount={5}    // Viewport dışında fazladan render edilecek satır
    >
      {Row}
    </List>
  );
}

// 10.000 ürün: Normal render ~500ms, Virtualized ~5ms
Bilgi: React 18'in useTransition hook'u, düşük öncelikli state güncellemelerini işaretleyerek UI'ın responsive kalmasını sağlar. Arama filtreleme gibi yoğun render işlemleri için startTransition kullanarak kullanıcı girişinin bloke olmasını önleyebilirsiniz.

React 18+ Concurrent Features

import { useState, useTransition, useDeferredValue } from 'react';

function SearchProducts() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  // useDeferredValue: Arama sonuçlarını geciktir
  const deferredQuery = useDeferredValue(query);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => {
          // Input güncellemesi yüksek öncelikli (anında)
          setQuery(e.target.value);
        }}
        placeholder="Ürün ara..."
      />
      {isPending && <span>Aranıyor...</span>}
      {/* Liste düşük öncelikli render - UI donmaz */}
      <ProductList query={deferredQuery} />
    </div>
  );
}

Sonuç

React performans optimizasyonu, önce ölçüm sonra iyileştirme prensibine dayanır. React DevTools Profiler ile sorunlu bileşenleri tespit edin, gereksiz re-render'ları composition pattern veya React.memo ile önleyin, pahalı hesaplamaları useMemo ile cache'leyin ve büyük listelerde virtualization kullanın. Erken optimizasyondan kaçının; Profiler'ın gösterdiği gerçek darboğazlara odaklanın.

Kaynaklar

İlgili Yazılar