React'te Performans: Re-render'ı Anlamak, memo, useMemo, useCallback, React Compiler ve Profiler
Re-render'ı azaltmak ancak ölçülen bir maliyet varsa işe yarar ve çoğu zaman asıl maliyet React'in değil tarayıcının DOM işidir. Bookstore'un 5.000 satırlık stok sayfasında memo ve useCallback'in satır render'larını 5.000'den 1'e indirdiğini, inline callback'in bunu sildiğini, React Compiler'ın render süresini yarıya indirdiğini ama tıklamanın 400 ms'de kaldığını, virtualization'ın ise onu 40 ms'ye düşürdüğünü; ayrıca useDeferredValue ve Activity'nin etkisini ölçüyoruz.
Sürüm notu: Bu yazıdaki örnekler React 19.3, React Compiler 1.0 (babel-plugin-react-compiler), @vitejs/plugin-react 6.3, @rolldown/plugin-babel 0.3, TanStack Virtual 3.14, TypeScript 7.0, Vite 8.3, Oxlint 1.93 ve Node.js 24 ile denenmiştir; süreler React'in profiling build'inde, Playwright 1.65 ile headless Chromium'da CPU 4 kat yavaşlatılarak ölçülmüştür. Mutlak milisaniyeler makineye göre değişir; karşılaştırılması gereken, aynı koşudaki sürümler arasındaki fark.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) back office'ine bir stok sayfası ekliyoruz: üç depodaki 5.000 stok lotu bir tabloda, üstte başlık ve yazara göre bir filtre, her satırda lotu işaretlemek için bir "Pin" butonu. API kopyamızda 20 kitap olduğu için lotları bu 20 kitaptan üreten bir test verisi fonksiyonu kullanıyoruz; amaç render maliyetini ölçülebilir kılmak.
Kullanıcı ilk satırdaki "Pin"e basıyor. Değişen tek bir satırın rengi. Kaç StockRow render edilir? memo ve useCallback eklenince kaç? React Compiler açılınca kaç? En önemlisi: bunlardan hangisi tıklamayı kullanıcı için gerçekten hızlandırır? Son sorunun cevabı ilk üçünden beklenen şey değil.
Ölçmeden Optimize Etme: Profiler ve Profiling Build
Dördüncü yazıda render sayısını component'lere console.log koyarak saymıştık. Sayı tek başına bir şey söylemez; asıl soru render'ın ne kadar sürdüğü ve kullanıcının ne kadar beklediği. React'in <Profiler> component'i, sardığı ağacın her commit'te render'ına harcanan süreyi bildiriyor:
import type { ProfilerOnRenderCallback } from 'react';
// <Profiler> bunu geliştirmede ve profiling build'de çağırır; normal production build hiç çağırmaz
export const reportRender: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration) => {
console.debug(`[profiler] ${id} ${phase} actual=${actualDuration.toFixed(1)}ms base=${baseDuration.toFixed(1)}ms`);
};
Profiler normal production build'de kapalı; react.dev profiling için ayrı bir build önerdiğini belirtiyor. Vite'ta bunu bir mode ile yaptık: vite build --mode profiling, react-dom/client'ı react-dom/profiling'e yönlendiriyor:
import babel from '@rolldown/plugin-babel'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// Dev proxy için Bookstore.Api adresi (README varsayılanı: http://localhost:5080)
const apiTarget = process.env.BOOKSTORE_API ?? 'http://localhost:5080'
// https://vite.dev/config/
export default defineConfig(({ mode }) => ({
// React Compiler 1.0: build sırasında otomatik memoization (react.dev, Vite kurulumu)
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
resolve: {
// '@/...' import'ları tsconfig.app.json'daki "paths"ten gelir
tsconfigPaths: true,
// `vite build --mode profiling`: <Profiler> süreleri açık bir production build
alias: mode === 'profiling' ? [{ find: 'react-dom/client', replacement: 'react-dom/profiling' }] : [],
},
server: {
proxy: {
// /api/books -> http://localhost:5080/api/books, tarayıcı için aynı origin
'/api': { target: apiTarget, changeOrigin: true },
},
},
}))
Kullanıcının beklediği süreyi ise tarayıcı ölçüyor: Event Timing API'si her etkileşimin olaydan bir sonraki boyamaya kadar geçen süresini veriyor. Bu, Core Web Vitals'taki INP'nin (Interaction to Next Paint; kullanıcı etkileşiminden sonraki ilk boyamaya kadar geçen süre) dayandığı ölçüm. Playwright her koşuda filtreye "dune" yazdı ve bir lotu pin'ledi; "React render" Profiler'ın toplamı, "slowest interaction" ise en yavaş etkileşim. React DevTools'un Profiler sekmesi ve React 19.2 ile Chrome DevTools'a gelen Performance Tracks aynı bilgiyi görsel olarak veriyor; headless ortamda onları kullanamadığımız için bu yazıdaki sayılar bu iki API'den.
Başlangıç kodu, şimdiye kadarki yazılardaki gibi hiçbir memoization içermiyor:
import type { StockLot } from '@/features/admin/stockLots';
type StockRowProps = {
lot: StockLot;
pinned: boolean;
onTogglePin: (lotId: string) => void;
};
export function StockRow({ lot, pinned, onTogglePin }: StockRowProps) {
return (
<tr className={pinned ? 'pinned' : undefined}>
<td>{lot.lotId}</td>
<td>{lot.title}</td>
<td>{lot.author}</td>
<td>{lot.warehouse}</td>
<td className="num">{lot.onHand}</td>
<td>
<button type="button" aria-pressed={pinned} aria-label={`Pin ${lot.lotId}`} onClick={() => onTogglePin(lot.lotId)}>
{pinned ? 'Unpin' : 'Pin'}
</button>
</td>
</tr>
);
}
export function StockPage() {
const [lots] = useState(() => makeStockLots(5000));
const [filter, setFilter] = useState('');
const [pinned, setPinned] = useState<ReadonlySet<string>>(() => new Set());
const term = filter.trim().toLowerCase();
const visible = lots.filter((lot) => lot.title.toLowerCase().includes(term) || lot.author.toLowerCase().includes(term));
memo, useCallback ve useMemo
memo bir component'i sarıyor: props'lar bir önceki render'dakilerle Object.is ile aynıysa component çağrılmıyor. Ama bir props her render'da yeni bir fonksiyonsa karşılaştırma her seferinde "farklı" der; useCallback fonksiyonu render'lar arasında sabit tutuyor. useMemo ise bir hesabın sonucunu bağımlılıkları değişene kadar saklıyor:
// memo: lot, pinned ve onTogglePin öncekiyle aynıysa render'ı atla (her props için Object.is)
export const StockRow = memo(function StockRow({ lot, pinned, onTogglePin }: StockRowProps) {
// useMemo: 5.000 lotu yalnız filtre metni değişince yeniden filtrele
const visible = useMemo(() => {
const term = filter.trim().toLowerCase();
return lots.filter((lot) => lot.title.toLowerCase().includes(term) || lot.author.toLowerCase().includes(term));
}, [lots, filter]);
// useCallback: her render'da aynı fonksiyon, böylece memo(StockRow) satırları atlayabilir
const togglePin = useCallback((lotId: string) => {
setPinned((current) => {
const next = new Set(current);
if (next.has(lotId)) {
next.delete(lotId);
} else {
next.add(lotId);
}
return next;
});
}, []);
Üçüncü sürümde her şey aynı, yalnız callback bir arrow function ile sarılmış; dördüncüsünde elle memoization yok ama React Compiler açık. Her satıra bir sayaç koyup kaç kez çağrıldığını saydık:
{/* YANLIŞ: her render'da yeni bir arrow function memo(StockRow)'u boşa çıkarır */}
<Profiler id="StockTable" onRender={reportRender}>
<StockTable lots={visible} pinned={pinned} onTogglePin={(lotId) => togglePin(lotId)} />
no memo type "dune": StockRow rendered 4250 time(s)
no memo pin one lot: StockRow rendered 5000 time(s)
memo + useCallback type "dune": StockRow rendered 0 time(s)
memo + useCallback pin one lot: StockRow rendered 1 time(s)
memo, inline callback type "dune": StockRow rendered 4250 time(s)
memo, inline callback pin one lot: StockRow rendered 5000 time(s)
React Compiler type "dune": StockRow rendered 4250 time(s)
React Compiler pin one lot: StockRow rendered 5000 time(s)
memo ile useCallback birlikte satır render'larını 5.000'den 1'e indirdi. Callback'i arrow function ile sarmak bütün kazancı sildi: memo var ama her satır her seferinde yeni bir onTogglePin aldığı için hiçbir satır atlanmadı. Bu üç araç birlikte çalışıyor; biri eksikse diğerleri işe yaramıyor ve kod yine de karmaşıklaşmış oluyor. useMemo'nun payı burada küçük: 5.000 elemanlık bir array'i filtrelemek profiling build'de bir milisaniyenin altında; react.dev'in useMemo sayfasında "hesap pahalı mı?" sorusunu ölçerek cevaplamayı önermesinin sebebi bu.
React Compiler 1.0
React Compiler, Ekim 2025'te 1.0 olarak stable çıkan bir build zamanı aracı. Component ve hook'larını analiz ediyor ve değerleri, JSX'i (JavaScript içinde HTML'e benzeyen syntax) ve fonksiyonları otomatik olarak memoize eden kod üretiyor. @vitejs/plugin-react 6 ile kurulumu yukarıdaki config'teki tek satır ve iki paket:
npm install -D babel-plugin-react-compiler@1.0.0 @rolldown/plugin-babel@0.3.1
Yukarıdaki sayaç çıktısında compiler'lı sürüm 5.000 satırın hepsini çağırdı. Bu bir hata değil, compiler'ın ne yaptığını anlamak için önemli bir nokta: compiler component'leri memo ile sarmıyor. Her component'in içinde, props'lar değişmediyse bir önceki JSX'i cache'ten döndürüyor; böylece React o satırın altına inmiyor. Ama tabloyu oluşturan map, pinned değiştiği için yeniden çalışıyor ve her satır için yeni bir element üretiyor; sayaç çıktısına göre compiler bu döngünün adımlarını ayrı ayrı memoize etmedi. Sonuç süre tablosunda:
no memo type "dune": React render 36 ms | slowest interaction 240 ms
no memo pin one lot: React render 32 ms | slowest interaction 440 ms
memo + useCallback type "dune": React render 7 ms | slowest interaction 184 ms
memo + useCallback pin one lot: React render 6 ms | slowest interaction 392 ms
React Compiler type "dune": React render 17 ms | slowest interaction 192 ms
React Compiler pin one lot: React render 15 ms | slowest interaction 400 ms
Compiler + deferred type "dune": React render 16 ms | slowest interaction 64 ms
Compiler + deferred pin one lot: React render 16 ms | slowest interaction 400 ms
Compiler + virtualized type "dune": React render 5 ms | slowest interaction 24 ms
Compiler + virtualized pin one lot: React render 0 ms | slowest interaction 40 ms
final (all of them) type "dune": React render 6 ms | slowest interaction 16 ms
final (all of them) pin one lot: React render 0 ms | slowest interaction 40 ms
React render süresi compiler ile 32 ms'den 15 ms'ye indi; elle yazılan memo ile 6 ms. Compiler elle yazılan kadar keskin değil ama component kodunda tek satır değiştirmeden sürenin yarısından fazlasını kazandırdı. Asıl uyarıcı olan sağdaki sütun: tıklama sürümlerin hepsinde 400 ms civarında kaldı. Önce Tahmin Et'teki son sorunun cevabı bu: React render'ı bu sayfanın darboğazı değil.
Compiler'ın varsaydığı şeyler, bu seride baştan beri uyduğumuz kurallar: render saf olmalı (aynı props ve state ile aynı çıktı), props ve state mutate edilmemeli, hook kuralları ihlal edilmemeli. Bu kurallara uymayan bir component'i compiler sessizce atlıyor, bozmuyor. Onuncu yazıdaki Oxlint uyarısını bu yüzden temizlemiştik; bu yazıda da tablodaki virtualizer aynı uyarıyı verdi:
> bookstore-react@0.0.0 lint
> oxlint
src/features/admin/StockTable.tsx:17:23: warning react(incompatible-library): Use of incompatible library help: This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized
TanStack Virtual'ın useVirtualizer'ı her render'da değişen fonksiyonlar döndürüyor; compiler bu component'i memoize etmiyor. Tablo component'i küçük ve satırlar yine derleniyor, bu yüzden kabul ettik ve uyarıyı gerekçesiyle bir yorumla kapattık. Bir component'i bilerek dışarıda tutmak için compiler'ın "use no memo" direktifi var; bir hatanın compiler'dan gelip gelmediğini anlamak için geçici olarak kullanılıyor, kalıcı bir çözüm değil. react.dev yeni kodda memoization için compiler'a güvenmeyi, mevcut useMemo ve useCallback'leri ise yerinde bırakmayı ya da dikkatle test ederek kaldırmayı öneriyor; kaldırmak compiler'ın çıktısını değiştirebiliyor.
Asıl Maliyet: 5.000 Satırlık DOM ve Virtualization
Tıklamanın 400 ms'si nereye gidiyor? React yalnız bir satırın class'ını değiştirse bile tarayıcı 5.000 satırlık bir tablonun stilini ve yerleşimini yeniden hesaplıyor. Filtreye ilk harf yazılınca da binlerce <tr> DOM'dan (tarayıcının sayfayı tuttuğu node ağacı) siliniyor. Bu maliyet Profiler'da görünmüyor, çünkü React'in değil tarayıcının işi. Çözüm daha az render değil, daha az DOM: virtualization, yalnız görünen satırları DOM'a koyuyor.
npm install @tanstack/react-virtual@3.14.26
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
import { StockRow } from '@/features/admin/StockRow';
import type { StockLot } from '@/features/admin/stockLots';
type StockTableProps = {
lots: StockLot[];
pinned: ReadonlySet<string>;
onTogglePin: (lotId: string) => void;
};
const rowHeight = 30;
// Virtualized: DOM'da yalnız scroll kutusunun içindeki satırlar (ve birkaç fazlası) var
export function StockTable({ lots, pinned, onTogglePin }: StockTableProps) {
const scrollBox = useRef<HTMLDivElement>(null);
// Bilinen ve kabul edilen durum: compiler bu küçük component'i atlar; aşağıdaki StockRow yine derlenir
// oxlint-disable-next-line react/incompatible-library
const virtualizer = useVirtualizer({
count: lots.length,
getScrollElement: () => scrollBox.current,
estimateSize: () => rowHeight,
overscan: 10,
});
const items = virtualizer.getVirtualItems();
// Üstteki ve alttaki boş satırlar scrollbar'ı listenin tamamına göre boyutlar
const before = items.length > 0 ? items[0].start : 0;
const after = items.length > 0 ? virtualizer.getTotalSize() - items[items.length - 1].end : 0;
return (
<div ref={scrollBox} className="stock-scroll">
<table className="stock">
<thead>
<tr><th>Lot</th><th>Title</th><th>Author</th><th>Warehouse</th><th>On hand</th><th /></tr>
</thead>
<tbody>
{before > 0 && <tr style={{ height: before }} aria-hidden="true" />}
{items.map((item) => {
const lot = lots[item.index];
return <StockRow key={lot.lotId} lot={lot} pinned={pinned.has(lot.lotId)} onTogglePin={onTogglePin} />;
})}
{after > 0 && <tr style={{ height: after }} aria-hidden="true" />}
</tbody>
</table>
</div>
);
}
600 piksellik kutuda 20 satır görünüyor, overscan ile birlikte DOM'da 30 satır var; üstteki ve alttaki boş satırlar scrollbar'ın 5.000 satırlık listeye göre boyutlanmasını sağlıyor. Süre tablosundaki "Compiler + virtualized" satırları sonucu gösteriyor: tıklama 400 ms'den 40 ms'ye, yazma 192 ms'den 24 ms'ye indi. Hiçbir memoization değişikliği bu ölçekte bir kazanç sağlamadı. Virtualization'ın bedeli de var: sayfa içi arama (Ctrl+F) yalnız DOM'daki satırları bulur, satır yükseklikleri değişkense ölçüm gerekir ve ekran okuyucular tablonun tamamını göremez. Erişilebilirlik tarafını Accessibility (a11y) serisinde, tarayıcının boyama maliyetini Web Performansı serisindeki "INP'yi Optimize Etmek: Input Delay, Processing ve Presentation Delay, Long Task'ları Bölmek ve Angular'da Akıcılık" yazısında ayrıntılı ele aldık.
useDeferredValue ve useTransition ile Akıcı Filtre
Virtualization yapamadığın durumlar var: bir grafik, bir harita ya da ölçüsü belli olmayan bir içerik. Orada başka bir araç devreye giriyor: güncellemeleri öncelik sırasına koymak. Kullanıcının yazdığı harf hemen görünmeli; listenin güncellenmesi bekleyebilir.
function AllLotsTab({ lots, pinned, onTogglePin }: AllLotsTabProps) {
const [filter, setFilter] = useState('');
// Input `filter`'ı hemen gösterir; liste React meşgulken geride kalabilen `deferredFilter`'ı izler
const deferredFilter = useDeferredValue(filter);
const term = deferredFilter.trim().toLowerCase();
const visible = lots.filter((lot) => lot.title.toLowerCase().includes(term) || lot.author.toLowerCase().includes(term));
return (
<>
<input type="search" aria-label="Filter stock" placeholder="Filter by title or author" value={filter}
onChange={(e) => setFilter(e.target.value)} />
<p className="muted" role="status">{visible.length} lot(s){filter !== deferredFilter ? ' · updating…' : ''}</p>
<Profiler id="StockTable" onRender={reportRender}>
<StockTable lots={visible} pinned={pinned} onTogglePin={onTogglePin} />
</Profiler>
useDeferredValue önce eski değerle hızlı bir render yapıyor, input yeni harfi gösteriyor ve ekran boyanıyor; ardından yeni değerle bir arka plan render'ı başlatıyor. Kullanıcı bu sırada bir harf daha yazarsa React yarım kalan render'ı bırakıp yenisine geçiyor. Süre tablosunda "Compiler + deferred" sürümü virtualization olmadan, 5.000 satırlık tam tabloyla ölçüldü: yazarken en yavaş etkileşim 192 ms'den 64 ms'ye indi. Liste aynı sürede güncelleniyor ama kullanıcı yazarken beklemiyor. Pin tıklaması değişmedi, çünkü o güncelleme ertelenmiyor.
useTransition aynı mekanizmanın state'i set eden tarafı: startTransition(() => setFilter(value)) bir güncellemeyi "acil değil" diye işaretliyor ve isPending veriyor. Değeri sen set ediyorsan useTransition, değeri props ya da başka bir yerden alıyorsan useDeferredValue. Dokuzuncu yazıda gördüğümüz gibi React Router gezinmeleri de transition içinde çalıştırıyor.
Activity ile Gizli UI'ı Korumak
Stok sayfasında iki sekme var: tüm lotlar ve pin'lenenler. Sekmeyi koşullu render ile değiştirirsen gizlenen sekme unmount oluyor; filtre metni, scroll konumu ve state'i kayboluyor. React 19.2 ile gelen <Activity> bunun yerine ağacı gizliyor:
<div className="tabs" role="tablist">
<button type="button" role="tab" aria-selected={tab === 'all'} onClick={() => setTab('all')}>All lots</button>
<button type="button" role="tab" aria-selected={tab === 'pinned'} onClick={() => setTab('pinned')}>Pinned ({pinned.size})</button>
</div>
{/* Gizli sekmeler state'lerini ve DOM'larını korur; effect'leri yeniden gösterilene kadar cleanup edilir */}
<Activity mode={tab === 'all' ? 'visible' : 'hidden'}>
<AllLotsTab lots={lots} pinned={pinned} onTogglePin={togglePin} />
</Activity>
<Activity mode={tab === 'pinned' ? 'visible' : 'hidden'}>
<StockTable lots={lots.filter((lot) => pinned.has(lot.lotId))} pinned={pinned} onTogglePin={togglePin} />
</Activity>
Kullanıcı "dune" diye filtreleyip ilk lotu pin'ledi, tabloyu aşağı kaydırdı, "Pinned" sekmesine geçip geri döndü:
<Activity> before: filter="dune" status="250 lot(s)" scrollTop=3000
<Activity> Pinned tab: LOT-00001 Dune
<Activity> back: filter="dune" status="250 lot(s)" scrollTop=3000
conditional before: filter="dune" status="250 lot(s)" scrollTop=3000
conditional Pinned tab: LOT-00001 Dune
conditional back: filter="" status="5000 lot(s)" scrollTop=0
Activity ile filtre, sonuç sayısı ve scroll konumu korundu; koşullu render ile hepsi sıfırlandı. Gizli moddaki ağaç display: none ile DOM'da duruyor, effect'leri cleanup ediliyor (virtualizer'ın scroll listener'ı gibi) ve görünür olunca yeniden kuruluyor. Gizliyken yeni props gelirse yine render ediliyor, ama sayfanın geri kalanından daha düşük öncelikle. Bedeli bellek: gizli sekmenin DOM'u ve state'i yaşamaya devam ediyor; onlarca gizli ekranı tutmak için değil, kullanıcının sık geçtiği birkaç görünüm için.
Üretimde Dikkat
Ölçümü production'a yakın koşullarda yap. Development build'de StrictMode çift render ediyor ve React ek kontroller çalıştırıyor; süreler gerçeği yansıtmaz. Bu yazıdaki her sayı profiling build'den. Kullanıcıların gerçek deneyimi için alan verisi (RUM: Real User Monitoring; gerçek kullanıcı tarayıcılarından toplanan ölçümler) gerekiyor; Web Performansı serisinin ilk yazısı "Web Performansını Doğru Ölçmek: Lab vs Field, CrUX, p75 ve Angular'da web-vitals ile RUM" bunu anlatıyor.
Code splitting de bir performans aracı. Stok sayfası lazy bir route; production build'de kendi chunk'ına ayrıldı:
> bookstore-react@0.0.0 build
> tsc -b && vite build
vite v8.3.8 building client environment for production...
transforming...
✓ 297 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.53 kB │ gzip: 0.32 kB
dist/assets/index-DxuAccDu.css 3.19 kB │ gzip: 1.09 kB
dist/assets/checkoutLoader-CHmDVjn0.js 0.24 kB │ gzip: 0.20 kB
dist/assets/react-DvXU6kJ2.js 8.41 kB │ gzip: 3.23 kB
dist/assets/StockPage-0KlSAsKS.js 40.14 kB │ gzip: 12.89 kB
dist/assets/CheckoutPage-Dwr05end.js 132.78 kB │ gzip: 40.46 kB
dist/assets/index-VAip40uS.js 368.33 kB │ gzip: 116.41 kB
✓ built in 443ms
TanStack Virtual'la birlikte stok sayfası 40 KB, onuncu yazıdan beri react-hook-form ve zod'u taşıyan ödeme sayfası 133 KB; ikisi de ana bundle'da değil. Katalog için gelen kullanıcı bu kodu hiç indirmiyor. react-*.js adlı küçük chunk compiler'ın runtime yardımcıları; birden çok chunk kullandığı için ayrı bir dosyaya çıkarılmış.
Compiler'ı mevcut bir projede kademeli aç. react.dev'de bunun için ayrı bir kademeli geçiş rehberi var; compiler'ın kural ihlallerini bulan lint kuralları da eslint-plugin-react-hooks'un önerilen preset'leriyle geliyor ve compiler kurulmadan da çalışıyor. Compiler kodun davranışını değiştirmemeli; değiştiriyorsa genellikle kurallara uymayan bir component vardır (render sırasında mutation, ref okumak gibi).
Sık Düşülen Tuzaklar
Ölçmeden memoize etmek: her component'i memo ile sarmak, her fonksiyonu useCallback'e koymak kodu okunmaz yapar ve çoğu zaman yukarıdaki inline callback örneğindeki gibi bir yerden delinir. Önce Profiler, sonra gerekiyorsa memoization; yeni kodda önce compiler.
Render süresini kullanıcının beklediği süre sanmak: Profiler yalnız React'in render'ını ölçer. Bizim örnekte asıl maliyet tarayıcının 5.000 satırı yeniden yerleştirmesiydi; bunu Event Timing ya da DevTools'un Performance paneli gösterir.
memo'nun bir object ya da array props'u yüzünden çalışmaması: render içinde oluşturulan { ... } ve [...] her seferinde yeni referanstır, tıpkı inline fonksiyonlar gibi.
useMemo'yu doğruluk için kullanmak: React bir cache'i gerektiğinde atabilir; useMemo bir performans ipucudur, bir değerin "bir kez" hesaplanacağının garantisi değil. Bir kez oluşturulması gereken değer için useState'in lazy initializer'ı (stok listemizdeki gibi) ya da useRef kullan.
Küçük Bir Deneme
★☆☆ overscan'ı 0 ve 50 yapıp hızlı scroll'da boş satır görüp görmediğini ve DOM'daki satır sayısını karşılaştır.
★★☆ Filtreyi useDeferredValue yerine useTransition ile yaz: input'u ayrı bir state'e bağla, listeyi besleyen değeri startTransition içinde set et ve isPending ile bir "updating…" göstergesi ekle. Hangisi daha az kodla aynı sonucu veriyor?
★★★ Katalogda sepete eklemede sekiz kitap kartının hepsi render ediliyordu. Compiler açıkken bunu Profiler ile tekrar ölç: kaç BookCard render ediliyor? Yalnız değişen kartın render edilmesi için BookCard'ı yine memo ile sarmak gerekiyor mu, yoksa props'lardan biri (onAdd, quantityInCart) mi engel oluyor?
Performansı ölçerek iyileştirdik: memoization'ın, compiler'ın, önceliklendirmenin ve virtualization'ın her biri farklı bir maliyeti düşürüyor ve hangisinin işe yarayacağını ancak ölçüm söylüyor. Sıradaki yazıda bu davranışları kalıcı hâle getireceğiz: Vitest ve React Testing Library ile component'leri kullanıcı gibi test edecek, sepet hesabını saf bir fonksiyon olarak doğrulayacak ve MSW (Mock Service Worker; istekleri network seviyesinde yakalayan bir kütüphane) ile Bookstore API'yi taklit edeceğiz.