Kenapa ini penting
Di modul sebelumnya kamu belajar memperbaiki performa dengan mengatur struktur komponen. Kadang struktur tidak bisa diubah: komponen berat memang harus menerima props dari parent yang sering berubah, atau ada perhitungan mahal yang dihitung ulang setiap render. Untuk kasus itu React menyediakan alat memoization: memo, useMemo, dan useCallback. Memoization artinya "mengingat hasil sebelumnya dan memakainya lagi kalau masukannya sama". Alat ini kuat, tapi sering dipakai berlebihan sehingga kode jadi rumit tanpa manfaat. Ditambah lagi, sekarang ada React Compiler (versi stabil 1.0) yang bisa melakukan memoization otomatis. Modul ini membahas ketiganya, kapan perlu, kapan berlebihan, dan posisinya setelah ada compiler.
Konsep inti
memo: lewati render jika props sama
memo(Komponen) membungkus komponen sehingga React melewati render ulang jika semua props-nya sama dengan render sebelumnya. Perbandingan dilakukan dangkal (shallow) dengan Object.is untuk setiap props: angka dan string dibandingkan nilainya, tapi object, array, dan fungsi dibandingkan referensinya.
Inilah jebakan terbesar: setiap render parent, { a: 1 }, [1, 2], atau () => {} yang ditulis langsung di JSX adalah nilai baru dengan referensi baru. Jadi memo gagal bekerja, karena props dianggap berubah.
Analogi: satpam yang hanya mengecek nomor kartu identitas, bukan wajah. Kalau setiap hari kamu datang membawa kartu baru (walaupun orangnya sama), satpam tetap menganggap kamu tamu baru.
useMemo: ingat hasil perhitungan
useMemo(() => hitung(a, b), [a, b]) menjalankan fungsi dan menyimpan hasilnya. Pada render berikutnya, kalau a dan b tidak berubah, React mengembalikan hasil yang tersimpan tanpa menghitung ulang. Kegunaannya ada dua:
1. Melewati perhitungan mahal (memfilter/mengurutkan ribuan data).
2. Menjaga referensi object/array tetap sama agar memo di anak atau dependency effect tidak terpicu.
useCallback: ingat fungsi
useCallback(fn, deps) mengembalikan fungsi yang sama (referensi sama) selama dependensinya tidak berubah. Ini setara dengan useMemo(() => fn, deps). Kegunaan utamanya: meneruskan fungsi ke anak yang dibungkus memo, atau fungsi yang dipakai sebagai dependency effect.
Kapan perlu, kapan berlebihan
| Situasi | Perlu memo? |
|---|---|
| Profiler menunjukkan komponen render lambat dan sering, padahal props sama | Ya, memo + stabilkan props |
| Perhitungan memakan waktu terasa (misalnya > 1 ms per render saat diukur) | Ya, useMemo |
Fungsi diteruskan ke anak yang di-memo |
Ya, useCallback |
| Komponen kecil dan murah (tombol, label) | Tidak, overhead perbandingan bisa lebih mahal dari render |
Perhitungan sederhana (a + b, filter 20 item) |
Tidak |
Props anak memakai children JSX yang selalu baru |
memo tidak berguna, children selalu berubah |
Ingat bahwa memoization punya biaya: memori untuk menyimpan, waktu membandingkan dependensi, dan kode yang lebih sulit dibaca. Dokumentasi React juga mengingatkan bahwa useMemo adalah optimasi performa, bukan jaminan. Kodemu harus tetap benar walaupun hasil memo dibuang.
React Compiler: memoization otomatis
React Compiler adalah alat build-time yang menganalisis komponen dan hook, lalu otomatis menambahkan memoization di tempat yang tepat. Statusnya stabil (paket babel-plugin-react-compiler versi 1.0.0) dan bisa dipakai di React 19 (juga React 17/18 dengan opsi target).
Rekomendasi resmi di react.dev:
- Kode baru: andalkan compiler untuk memoization, pakai useMemo/useCallback hanya sebagai escape hatch kalau butuh kontrol presisi (misalnya nilai yang dipakai sebagai dependency effect agar effect tidak berjalan berulang).
- Kode lama: biarkan memoization manual yang sudah ada, atau uji dengan hati-hati sebelum menghapusnya, karena menghapusnya bisa mengubah hasil kompilasi.
Compiler hanya bisa mengoptimasi kode yang mengikuti Rules of React: komponen murni, tidak mengubah props/state secara langsung, hook dipanggil di level atas. Komponen yang melanggar akan dilewati (aman, hanya tidak dioptimasi). Paket eslint-plugin-react-hooks (versi saat ini 7.1.1) memuat aturan-aturan yang membantu menemukan pelanggaran ini.
Cara memeriksa compiler aktif: buka React DevTools, komponen yang dioptimasi menampilkan badge "Memo ✨".
Setup di Vite 8 dengan @vitejs/plugin-react 6.x: opsi babel di dalam react() sudah dihapus di versi 6, jadi compiler dipasang lewat @rolldown/plugin-babel dan helper reactCompilerPreset (lihat contoh kode). Tutorial lama yang menulis react({ babel: { plugins: ['babel-plugin-react-compiler'] } }) hanya berlaku untuk plugin-react versi di bawah 6.
Contoh kode
Melihat masalah: memo gagal karena props fungsi baru setiap render.
import { memo, useState } from 'react';
const ItemList = memo(function ItemList({ items, onPilih }) {
console.log('ItemList render'); // pantau di console
return (
<ul>
{items.map((it) => (
<li key={it} onClick={() => onPilih(it)}>{it}</li>
))}
</ul>
);
});
export default function AppMasalah() {
const [hitung, setHitung] = useState(0);
const items = ['Apel', 'Jeruk', 'Mangga']; // array baru tiap render
const pilih = (it) => alert(it); // fungsi baru tiap render
return (
<>
<button onClick={() => setHitung(hitung + 1)}>Klik {hitung}</button>
<ItemList items={items} onPilih={pilih} />
</>
);
}
// klik tombol → console: "ItemList render" setiap kali. memo tidak membantu.
Perbaikan manual dengan useMemo dan useCallback:
import { memo, useCallback, useMemo, useState } from 'react';
const ItemList = memo(function ItemList({ items, onPilih }) {
console.log('ItemList render');
return (
<ul>
{items.map((it) => (
<li key={it} onClick={() => onPilih(it)}>{it}</li>
))}
</ul>
);
});
// konstanta di luar komponen: referensinya tidak pernah berubah, jadi bukan dependency
const SEMUA = ['Apel', 'Jeruk', 'Mangga', 'Anggur', 'Alpukat'];
export default function AppDiperbaiki() {
const [hitung, setHitung] = useState(0);
const [kata, setKata] = useState('');
// perhitungan + referensi array stabil selama "kata" sama
const items = useMemo(
() => SEMUA.filter((s) => s.toLowerCase().includes(kata.toLowerCase())),
[kata]
);
// fungsi stabil, tidak bergantung pada state apa pun
const pilih = useCallback((it) => alert(it), []);
return (
<>
<button onClick={() => setHitung(hitung + 1)}>Klik {hitung}</button>
<input value={kata} onChange={(e) => setKata(e.target.value)} />
<ItemList items={items} onPilih={pilih} />
</>
);
}
// klik tombol → ItemList TIDAK render ulang
// ketik "a" → ItemList render (items berubah) → Apel, Mangga, Anggur, Alpukat
Catatan: SEMUA sengaja ditaruh di luar komponen. Kalau array itu ditulis di dalam komponen, ia menjadi array baru setiap render, harus masuk dependency useMemo, dan memo akan selalu menghitung ulang.
Versi dengan React Compiler: tulis kode biasa, compiler yang mengurus.
# Vite 8 + @vitejs/plugin-react 6.x
npm install -D babel-plugin-react-compiler @rolldown/plugin-babel @babel/core
// vite.config.js
import { defineConfig } from 'vite';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';
export default defineConfig({
plugins: [
react(),
babel({ presets: [reactCompilerPreset()] }), // memoization otomatis saat build
],
});
// Dengan compiler aktif, kode polos ini sudah dioptimasi otomatis
import { useState } from 'react';
const SEMUA = ['Apel', 'Jeruk', 'Mangga', 'Anggur', 'Alpukat'];
function ItemList({ items, onPilih }) {
return (
<ul>
{items.map((it) => (
<li key={it} onClick={() => onPilih(it)}>{it}</li>
))}
</ul>
);
}
export default function AppCompiler() {
const [hitung, setHitung] = useState(0);
const [kata, setKata] = useState('');
const items = SEMUA.filter((s) => s.toLowerCase().includes(kata.toLowerCase()));
const pilih = (it) => alert(it);
return (
<>
<button onClick={() => setHitung(hitung + 1)}>Klik {hitung}</button>
<input value={kata} onChange={(e) => setKata(e.target.value)} />
<ItemList items={items} onPilih={pilih} />
</>
);
}
// React DevTools → AppCompiler dan ItemList menampilkan badge "Memo ✨"
Kesalahan umum
- Membungkus
memotapi meneruskan object/fungsi baru.memojadi tidak berguna. Stabilkan denganuseMemo/useCallback, pindahkan konstanta ke luar komponen, atau gunakan React Compiler. - Memo di mana-mana "biar aman". Komponen murah justru bisa sedikit lebih lambat karena biaya perbandingan, dan kode jadi sulit dibaca. Pakai hanya setelah diukur.
- Dependency array yang tidak lengkap.
useMemo(() => filter(data, kata), [kata])padahaldatajuga berubah membuat hasil basi. Sertakan semua nilai yang dipakai, dan biarkan linterreact-hooks/exhaustive-depsmembantu. - Mengandalkan
useMemountuk kebenaran logika. React boleh membuang cache memo. Kalau kode jadi salah tanpa memo, desainnya yang perlu diperbaiki. - Mengikuti tutorial compiler lama. Opsi
react({ babel: ... })sudah dihapus di@vitejs/plugin-react6. Pakai@rolldown/plugin-babel+reactCompilerPreset().
Latihan
- Mudah, Bungkus komponen
Headerdenganmemodan buktikan lewatconsole.logbahwa ia tidak re-render ketika state counter di parent berubah (Header tidak menerima props). - Sedang, Ada komponen
Tabelyang di-memomenerima propsonHapus. Parent juga punya state input. Perbaiki agar mengetik di input tidak me-render ulangTabel. - Menantang, Buat daftar 5.000 angka acak dan input filter "lebih besar dari X". Hitung waktu filter dengan
console.time, lalu gunakanuseMemoagar menekan tombol counter yang tidak berhubungan tidak menghitung ulang filter.
Lihat solusi
// 1.
import { memo, useState } from 'react';
const Header = memo(function Header() {
console.log('Header render');
return <h1>Toko Saya</h1>;
});
export default function App() {
const [n, setN] = useState(0);
return (
<>
<Header />
<button onClick={() => setN(n + 1)}>{n}</button>
</>
);
}
// "Header render" hanya muncul saat mount (dua kali di dev karena StrictMode), tidak saat klik
// 2.
import { memo, useCallback, useState } from 'react';
const Tabel = memo(function Tabel({ data, onHapus }) {
console.log('Tabel render');
return (
<ul>
{data.map((d) => (
<li key={d.id}>
{d.nama} <button onClick={() => onHapus(d.id)}>Hapus</button>
</li>
))}
</ul>
);
});
export default function App() {
const [data, setData] = useState([{ id: 1, nama: 'Sari' }, { id: 2, nama: 'Budi' }]);
const [catatan, setCatatan] = useState('');
// setData dengan updater → tidak butuh "data" di dependency
const hapus = useCallback((id) => setData((d) => d.filter((x) => x.id !== id)), []);
return (
<>
<input value={catatan} onChange={(e) => setCatatan(e.target.value)} />
<Tabel data={data} onHapus={hapus} />
</>
);
}
// 3.
import { useMemo, useState } from 'react';
const ANGKA = Array.from({ length: 5000 }, () => Math.floor(Math.random() * 10000));
export default function FilterAngka() {
const [batas, setBatas] = useState(5000);
const [klik, setKlik] = useState(0);
const hasil = useMemo(() => {
console.time('filter');
const h = ANGKA.filter((x) => x > batas).toSorted((a, b) => a - b);
console.timeEnd('filter');
return h;
}, [batas]);
return (
<>
<input type="number" value={batas} onChange={(e) => setBatas(Number(e.target.value))} />
<button onClick={() => setKlik(klik + 1)}>Klik tidak berhubungan: {klik}</button>
<p>{hasil.length} angka lebih besar dari {batas}</p>
</>
);
}
// ubah batas → console "filter: 0.8ms" (angka bervariasi)
// klik tombol → tidak ada log "filter" baru
Ringkasan
memomelewati render ulang jika props sama secara dangkal (Object.is).- Object, array, dan fungsi baru di setiap render membuat
memogagal; stabilkan denganuseMemodanuseCallback. - Pakai memoization setelah mengukur; komponen dan perhitungan murah tidak membutuhkannya.
- React Compiler 1.0 menambahkan memoization otomatis; untuk kode baru, react.dev menyarankan mengandalkannya dan memakai
useMemo/useCallbacksebagai escape hatch. - Di
@vitejs/plugin-react6, compiler dipasang lewat@rolldown/plugin-babel+reactCompilerPreset().