Kenapa ini penting
React sudah cepat untuk sebagian besar aplikasi, jadi optimasi bukan langkah pertama. Tapi ketika aplikasi membesar, gejala seperti ini mulai muncul: mengetik di input terasa tersendat, membuka halaman pertama kali lama karena file JavaScript berukuran beberapa megabyte, atau daftar seribu item terasa berat saat di-scroll. Untuk memperbaikinya, kamu harus paham kapan dan kenapa React me-render ulang, lalu mengukur sebelum mengubah apa pun. Modul ini membahas cara kerja re-render, React DevTools Profiler, teknik memecah state, dan code splitting dengan lazy + Suspense. Teknik memoization (memo, useMemo, useCallback, React Compiler) dibahas khusus di modul berikutnya.
Konsep inti
Kapan re-render terjadi?
Sebuah komponen di-render ulang ketika:
1. State-nya sendiri berubah (lewat setState atau dispatch).
2. Parent-nya re-render. Secara default, semua anak ikut di-render ulang, walaupun props-nya tidak berubah.
3. Context yang dibaca berubah nilainya.
Perhatikan poin 2: ini yang paling sering mengejutkan pemula. Banyak yang mengira komponen hanya re-render kalau props berubah. Faktanya, render ulang anak adalah perilaku default.
Penting juga: render tidak sama dengan mengubah DOM. Render artinya React memanggil fungsi komponenmu untuk menghasilkan JSX baru, lalu membandingkannya dengan hasil sebelumnya. DOM asli hanya diubah di bagian yang benar-benar berbeda. Jadi render ulang biasanya murah. Masalah muncul jika render itu sendiri mahal (misalnya memfilter 10.000 item) atau terjadi sangat sering di pohon komponen yang besar.
Analogi: re-render itu seperti guru yang memeriksa ulang seluruh lembar ujian setiap ada satu siswa mengganti jawaban. Memeriksa cepat, tapi kalau lembarnya ribuan dan pemeriksaan diulang setiap detik, guru kelelahan.
Ukur dulu dengan React DevTools Profiler
Pasang ekstensi browser React Developer Tools. Ada dua tab: Components dan Profiler.
- Di Profiler, klik tombol rekam, lakukan interaksi (misalnya mengetik), lalu stop. Kamu akan melihat flamegraph: setiap batang adalah komponen, lebarnya menunjukkan waktu render. Abu-abu berarti tidak di-render pada commit itu.
- Di pengaturan DevTools ada opsi "Highlight updates when components render". Komponen yang re-render akan berkedip dengan garis berwarna di layar. Cara cepat untuk melihat "kok semua ikut berkedip saat aku mengetik?".
- Opsi "Record why each component rendered while profiling" menunjukkan alasan render: props berubah, state berubah, atau parent render.
Aturan utamanya: jangan mengoptimasi berdasarkan firasat. Ukur, temukan komponen yang benar-benar lambat, perbaiki, lalu ukur lagi. Ukur juga di build production (npm run build && npm run preview), karena mode development memang lebih lambat dan StrictMode menggandakan render.
Memecah state: pindahkan state ke bawah
Teknik paling efektif dan sering dilupakan: taruh state sedekat mungkin dengan tempat dipakainya. Kalau state input pencarian berada di App, setiap ketikan me-render ulang seluruh App beserta semua anaknya, termasuk komponen berat yang tidak ada hubungannya. Pindahkan input dan state-nya ke komponen kecil sendiri, maka hanya komponen kecil itu yang re-render.
Teknik kedua: lift content up, yaitu meneruskan komponen berat sebagai children. Elemen children dibuat oleh parent di atasnya, jadi ketika state komponen pembungkus berubah, elemen children yang sama tidak ikut di-render ulang.
Teknik ketiga: pisahkan state yang tidak berhubungan. Satu object state raksasa { form, filter, modal } membuat semua bagian bergantung pada satu nilai. State terpisah (atau Context terpisah, lihat modul 6) membatasi dampak perubahan.
Code splitting dengan lazy dan Suspense
Vite menggabungkan kode menjadi bundle. Tanpa pengaturan, halaman admin, grafik, dan editor teks ikut terunduh walaupun user hanya membuka beranda. Code splitting memecah bundle menjadi potongan (chunk) yang diunduh saat dibutuhkan.
Di React caranya:
- const Grafik = lazy(() => import('./Grafik.jsx')): komponen baru diunduh saat pertama kali dirender. Komponen yang di-lazy harus diekspor sebagai default export.
- Bungkus dengan <Suspense fallback={...}> untuk menampilkan UI sementara selama file diunduh.
- Panggil lazy di level modul (di luar komponen), bukan di dalam fungsi komponen.
Kandidat terbaik untuk lazy: halaman/route yang jarang dibuka, modal berat, grafik, editor, peta.
Contoh kode
Masalah: state input di parent membuat komponen berat ikut re-render.
// SEBELUM, setiap ketikan me-render ulang DaftarBerat
import { useState } from 'react';
function DaftarBerat() {
const items = [];
for (let i = 0; i < 3000; i++) items.push(<li key={i}>Item {i}</li>);
return <ul>{items}</ul>;
}
export default function AppLambat() {
const [teks, setTeks] = useState('');
return (
<>
<input value={teks} onChange={(e) => setTeks(e.target.value)} />
<p>Kamu mengetik: {teks}</p>
<DaftarBerat /> {/* ikut render tiap huruf, padahal tidak memakai teks */}
</>
);
}
Solusi 1: pindahkan state ke bawah.
// SESUDAH, state teks hanya hidup di FormKetik
import { useState } from 'react';
function FormKetik() {
const [teks, setTeks] = useState('');
return (
<>
<input value={teks} onChange={(e) => setTeks(e.target.value)} />
<p>Kamu mengetik: {teks}</p>
</>
);
}
export default function AppCepat() {
return (
<>
<FormKetik />
<DaftarBerat /> {/* tidak lagi re-render saat mengetik */}
</>
);
}
Solusi 2: lift content up lewat children.
// Pembungkus punya state warna, tapi children dibuat oleh parent
import { useState } from 'react';
function PanelBerwarna({ children }) {
const [warna, setWarna] = useState('white');
return (
<div style={{ background: warna }}>
<button onClick={() => setWarna(warna === 'white' ? 'lightyellow' : 'white')}>Ganti warna</button>
{children} {/* elemen sama dari parent → tidak di-render ulang */}
</div>
);
}
export default function App() {
return (
<PanelBerwarna>
<DaftarBerat />
</PanelBerwarna>
);
}
Code splitting halaman dengan lazy + Suspense:
// App.jsx
import { lazy, Suspense, useState } from 'react';
import Beranda from './pages/Beranda.jsx'; // dimuat langsung
// lazy di level modul; file Laporan.jsx wajib export default
const Laporan = lazy(() => import('./pages/Laporan.jsx'));
export default function App() {
const [halaman, setHalaman] = useState('beranda');
return (
<>
<nav>
<button onClick={() => setHalaman('beranda')}>Beranda</button>
<button onClick={() => setHalaman('laporan')}>Laporan</button>
</nav>
<Suspense fallback={<p>Memuat halaman...</p>}>
{halaman === 'beranda' ? <Beranda /> : <Laporan />}
</Suspense>
</>
);
}
// npm run build → Vite membuat chunk terpisah, misalnya:
// → dist/assets/index-abc123.js
// → dist/assets/Laporan-def456.js (baru diunduh saat tombol Laporan diklik)
// pages/Laporan.jsx
export default function Laporan() {
return <h1>Laporan Penjualan</h1>;
}
Kesalahan umum
- Mengoptimasi tanpa mengukur. Menebak bagian yang lambat sering salah. Rekam dengan Profiler dulu.
- Mengukur performa di mode development. Mode dev lebih lambat dan StrictMode merender dua kali. Pakai
npm run buildlalunpm run previewuntuk angka yang realistis. - Memanggil
lazydi dalam komponen.const X = lazy(...)di dalam fungsi komponen membuat komponen baru setiap render, sehingga state hilang dan file dimuat ulang. Taruh di level modul. - Lupa
Suspenseuntuk komponen lazy. TanpaSuspensedi atasnya, React tidak tahu apa yang ditampilkan selama memuat. Pastikan ada boundary di atasnya. - Menaruh semua state di
App. Ini membuat setiap perubahan kecil merender seluruh aplikasi. Turunkan state ke komponen yang memakainya.
Latihan
- Mudah, Aktifkan "Highlight updates when components render" di React DevTools, lalu jalankan contoh
AppLambat. Catat komponen mana saja yang berkedip saat mengetik. - Sedang, Refactor komponen berikut agar mengetik di input tidak me-render
GrafikBerat:Appberisi statecatatan, input catatan, dan<GrafikBerat />. - Menantang, Buat aplikasi dengan tiga tab (
Profil,Statistik,Pengaturan) di manaStatistikdanPengaturandimuat denganlazy. Buat satuSuspensedengan skeleton sebagai fallback, lalu jalankannpm run builddan cek ada berapa file JS yang dihasilkan.
Lihat solusi
// 1. Hasil yang diharapkan (catatan):
// Saat mengetik di AppLambat, yang berkedip: AppLambat dan DaftarBerat.
// Setelah diubah ke AppCepat, yang berkedip hanya FormKetik.
// 2.
import { useState } from 'react';
function GrafikBerat() {
return <div>(grafik berat)</div>;
}
function InputCatatan() {
const [catatan, setCatatan] = useState('');
return <textarea value={catatan} onChange={(e) => setCatatan(e.target.value)} />;
}
export default function App() {
return (
<>
<InputCatatan />
<GrafikBerat />
</>
);
}
// 3. App.jsx
import { lazy, Suspense, useState } from 'react';
import Profil from './tabs/Profil.jsx';
const Statistik = lazy(() => import('./tabs/Statistik.jsx'));
const Pengaturan = lazy(() => import('./tabs/Pengaturan.jsx'));
function SkeletonTab() {
return <div style={{ height: 120, background: '#e5e7eb', borderRadius: 8 }} />;
}
export default function App() {
const [tab, setTab] = useState('profil');
return (
<>
{['profil', 'statistik', 'pengaturan'].map((t) => (
<button key={t} onClick={() => setTab(t)}>{t}</button>
))}
<Suspense fallback={<SkeletonTab />}>
{tab === 'profil' && <Profil />}
{tab === 'statistik' && <Statistik />}
{tab === 'pengaturan' && <Pengaturan />}
</Suspense>
</>
);
}
// npm run build → minimal 3 file JS: bundle utama + Statistik-xxx.js + Pengaturan-xxx.js
Ringkasan
- Komponen re-render ketika state-nya berubah, parent-nya re-render, atau context yang dibaca berubah.
- Render bukan berarti DOM diubah; React hanya mengubah bagian DOM yang berbeda.
- Selalu ukur dengan React DevTools Profiler, idealnya di build production.
- Turunkan state ke komponen yang memakainya, atau teruskan komponen berat sebagai
children. lazy+Suspensememecah bundle sehingga kode diunduh hanya saat dibutuhkan.