kodingup.camp

Beranda/React Mastery: Styling, Pola Komponen, Data & Optimasi

Strategi Memoization Komponen

Modul 12 dari 135 menit bacaComponent Memoization Strategies

Isi modul

    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

    1. Membungkus memo tapi meneruskan object/fungsi baru. memo jadi tidak berguna. Stabilkan dengan useMemo/useCallback, pindahkan konstanta ke luar komponen, atau gunakan React Compiler.
    2. 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.
    3. Dependency array yang tidak lengkap. useMemo(() => filter(data, kata), [kata]) padahal data juga berubah membuat hasil basi. Sertakan semua nilai yang dipakai, dan biarkan linter react-hooks/exhaustive-deps membantu.
    4. Mengandalkan useMemo untuk kebenaran logika. React boleh membuang cache memo. Kalau kode jadi salah tanpa memo, desainnya yang perlu diperbaiki.
    5. Mengikuti tutorial compiler lama. Opsi react({ babel: ... }) sudah dihapus di @vitejs/plugin-react 6. Pakai @rolldown/plugin-babel + reactCompilerPreset().

    Latihan

    1. Mudah, Bungkus komponen Header dengan memo dan buktikan lewat console.log bahwa ia tidak re-render ketika state counter di parent berubah (Header tidak menerima props).
    2. Sedang, Ada komponen Tabel yang di-memo menerima props onHapus. Parent juga punya state input. Perbaiki agar mengetik di input tidak me-render ulang Tabel.
    3. Menantang, Buat daftar 5.000 angka acak dan input filter "lebih besar dari X". Hitung waktu filter dengan console.time, lalu gunakan useMemo agar 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

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.