React performance artinya bikin React melakukan kerja yang memang perlu saja: gak render ulang komponen yang hasilnya sama, dan gak membiarkan kerja berat menahan ketikan user. Bayangin kamu ganti satu kata di halaman 1 sebuah buku, lalu printer mencetak ulang seluruh 200 halaman. memo itu printer yang mengecek dulu: "isi halaman ini sama dengan kemarin? Lewati." Tapi cek itu juga makan waktu, jadi cuma layak untuk halaman yang mahal dicetak.
🧪 Tebakan cepat:
ProductListsudah dibungkusmemo, tapi App mengirimonSelect={() => pilih(id)}yang ditulis langsung di JSX. Kamu klik tombol +1 keranjang.ProductListrender ulang atau gak? Tebak dulu, jawabannya ada di bagian "useCallback dan useMemo".
Kapan komponen render ulang
Render artinya React memanggil function komponenmu untuk menghitung tampilan, lalu mengubah DOM di bagian yang beda saja. Komponen render ulang kalau:
- state-nya sendiri berubah (
useState,useReducer), - parent-nya render ulang: semua child ikut render, walaupun props-nya sama persis,
- context yang dibacanya berubah.
"Props berubah" bukan pemicu tersendiri: props cuma bisa berubah karena parent render ulang. Render ulang biasanya murah (1 atau 2 ms). Jadi masalah kalau komponennya mahal, misalnya list 5.000 produk, dan render-nya sering.
Ukur dulu dengan React DevTools Profiler
Jangan menebak komponen mana yang lambat. Pasang ekstensi React DevTools, buka tab Profiler, tekan Record, lakukan interaksi yang terasa lambat, lalu Stop. Flamegraph-nya menunjukkan tiap komponen yang render dan berapa lama; yang abu-abu gak render. Di pengaturannya, nyalakan "Record why each component rendered" supaya tiap render ada alasannya.
Ini hasil dari Lab di halaman ini: toko dengan kotak cari, tombol +1 keranjang, dan ProductList berisi 5.000 produk. Satu render list makan sekitar 280 ms (di Lab disimulasikan, gak benar-benar membakar CPU).
| Interaksi | App render | ProductList render | Props ProductList berubah? |
|---|---|---|---|
| Ketik "k" | 1 | 1 | ya, query "" jadi "k" |
| Ketik "o" | 1 | 1 | ya, "k" jadi "ko" |
| Klik +1 keranjang 8 kali | 8 | 8 | tidak |
| Total | 10 | 10 | 2 dari 10, yaitu 20% |
Delapan render list itu sia-sia: 8 × 280 ms = 2.240 ms kerja tanpa hasil yang beda.
React.memo: lewati render kalau props sama
import { memo, useState } from 'react';
const ProductList = memo(function ProductList({ query }) {
const visible = products.filter((p) => p.name.includes(query)); // 5.000 produk, mahal
return (
<ul>
{visible.map((p) => <li key={p.id}>{p.name}</li>)}
</ul>
);
});
function App() {
const [query, setQuery] = useState('');
const [cart, setCart] = useState(0);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={() => setCart(cart + 1)}>+1 keranjang ({cart})</button>
<ProductList query={query} />
</>
);
}
memo(...)membungkus komponen. Sebelum render, React membandingkan tiap prop dengan render sebelumnya (perbandingan dangkal,Object.is). Semua sama? Render-nya dilewati.- Klik +1 keranjang: App render ulang, tapi
querygak berubah, jadiProductListdilewati. Dari tabel tadi, list cuma render 2 kali, bukan 10. key={p.id}: key yang stabil membuat React mengenali baris yang sama antar render. Index array sebagai key bikin baris salah dikenali saat list difilter atau diurutkan.
useCallback dan useMemo: jaga props tetap sama
ProductList tetap render ulang. Itu jawaban tebakannya. () => pilih(id) yang ditulis langsung di JSX adalah function baru di setiap render, jadi memo melihat prop onSelect berubah. Sama juga dengan object atau array yang dibuat di dalam render.
const handleSelect = useCallback((id) => setSelected(id), []); // function yang sama antar render
const sorted = useMemo(() => [...products].sort(byPrice), [products]); // dihitung ulang kalau products berubah
<ProductList query={query} items={sorted} onSelect={handleSelect} />
useCallback(fn, deps)menyimpan function yang sama selamadepsgak berubah.useMemo(() => hitung, deps)menyimpan nilai hasil hitungan: buat hitungan mahal, atau object/array yang dikirim ke komponenmemo.[...products].sort(...): salin dulu, karenasortmengubah array aslinya.
Ringkasnya: React.memo untuk komponen, useMemo untuk nilai, useCallback untuk function.
Input tetap responsif: useDeferredValue dan useTransition
Tanpa trik, tiap ketikan ikut menunggu list 280 ms selesai render, jadi jeda dari ketik sampai huruf muncul sekitar 290 ms. Batas INP yang good itu 200 ms.
const deferredQuery = useDeferredValue(query);
<ProductList query={deferredQuery} /> // ProductList tetap memo
React sekarang render dua kali per ketikan. Render pertama mendesak: input dapat huruf baru, deferredQuery masih nilai lama, dan ProductList dilewati memo. Render kedua jalan di background dengan nilai baru, dan bisa disela kalau kamu mengetik lagi.
| Mode di Lab | List render per ketikan | Jeda input |
|---|---|---|
| Tanpa apa-apa | 1, di render mendesak | sekitar 290 ms |
| useDeferredValue tanpa memo | 2, nilai lama dan baru | sekitar 290 ms |
| useDeferredValue + memo | 1, di background | sekitar 12 ms |
Kalau kamu sendiri yang memanggil setState-nya, pakai useTransition: startTransition(() => setFilter(value)) menandai update itu gak mendesak, dan isPending bisa dipakai untuk indikator loading.
Pola lain yang sering menolong
- State colocation: taruh state sedekat mungkin dengan komponen yang memakainya. Kalau
querycuma dipakai panel cari, simpan di panel itu, jadi mengetik gak bikin seluruh App render ulang. - List ratusan atau ribuan baris: pakai virtualization (
react-window, TanStack Virtual), cuma baris yang kelihatan yang dirender. Detailnya di materi Virtual Scrolling. - Komponen besar yang jarang dibuka:
const Chart = lazy(() => import('./Chart'))dengan<Suspense>, dibahas di materi Code Splitting. - React Compiler: di project React modern, compiler ini (versi 1.0 rilis 2025) menambahkan memoization otomatis saat build. Kamu jadi jarang perlu menulis
memo,useMemo, danuseCallbacksendiri, tapi tetap ukur dengan Profiler.
Versi latihan: kapan memo menguntungkan
shouldMemoize mengembalikan true hanya kalau ketiga syarat terpenuhi: renderCount > 5 (sering render), propsChangedPercent < 30 (props jarang berubah, jadi memo sering bisa melewati), dan computationMs > 10 (tiap render mahal).
| renderCount | propsChangedPercent | computationMs | Hasil |
|---|---|---|---|
| 10 | 20 | 15 | true |
| 3 | 20 | 15 | false, jarang render |
| 10 | 50 | 15 | false, props sering berubah |
| 10 | 20 | 5 | false, render-nya murah |
identifyUnneededReRenders menyaring komponen dengan propsChanged === false: yang render tanpa props berubah, calon React.memo. Dari Header, UserCard, dan Footer, hasilnya Header dan Footer.
⚠️ Jebakan yang sering ketemu
- Membungkus semua komponen dengan
memo. Membandingkan props juga ada biayanya. Di komponen murah, kodenya cuma jadi ribet. Ukur dulu. - Function, object, atau array baru sebagai prop komponen
memo.memojadi gak pernah melewati render. PakaiuseCallbackatauuseMemo. useDeferredValuetanpamemo. List tetap render di render yang mendesak, jadi input tetap tersendat.- Di latihan: pakai
>dan<, bukan>=dan<=.renderCount5 bukan "lebih dari 5", dan 30% bukan "kurang dari 30".
TL;DR: Komponen render ulang kalau state-nya berubah, parent-nya render ulang, atau context-nya berubah. Ukur dulu dengan React DevTools Profiler. React.memo melewati render kalau props sama, useCallback menjaga function tetap sama, useMemo menyimpan nilai. Function, object, atau array baru di setiap render membatalkan memo. useDeferredValue (atau useTransition) plus memo membuat input tetap responsif sementara list berat menyusul di background. Pakai key yang stabil, simpan state dekat pemakainya, virtualisasi list panjang, dan pertimbangkan React Compiler. Memo menguntungkan kalau komponen sering render, props-nya jarang berubah, dan render-nya mahal.