Optimasi Performa Aplikasi React: Tips dan Trik Pro
Pendahuluan
Performa adalah salah satu faktor paling kritis dalam pengembangan aplikasi web modern. Bayangkan kamu sedang membangun platform e-commerce seperti Tokopedia — jika halaman produk butuh 5 detik untuk dimuat, pengguna akan langsung pergi dan konversi penjualan anjlok drastis.
React secara default sudah cukup cepat, tapi ketika aplikasimu tumbuh besar dengan ratusan komponen, ribuan item list, dan data yang terus berubah, performa bisa menurun signifikan. Di artikel ini, kita akan membahas teknik-teknik lanjutan untuk mengoptimasi aplikasi React — mulai dari code splitting, virtualisasi list, penggunaan Profiler API, hingga optimasi bundle size.
Artikel ini mengasumsikan kamu sudah familiar dengan hooks dasar React. Jika belum, pastikan kamu memahami Tipe Data Dasar di TypeScript terlebih dahulu karena beberapa contoh kita akan menggunakan TypeScript.
Konsep Dasar
Mengapa Aplikasi React Bisa Lambat?
Sebelum mengoptimasi, kita perlu memahami penyebab utama performa buruk di React:
- Re-render berlebihan — komponen dirender ulang meski datanya tidak berubah
- Bundle size terlalu besar — semua kode dimuat sekaligus di awal
- List panjang tanpa virtualisasi — merender 10.000 item sekaligus ke DOM
- Memory leak — efek samping yang tidak dibersihkan
- Blocking main thread — komputasi berat yang membekukan UI
React 18 Concurrent Features
React 18 memperkenalkan mode concurrent yang memungkinkan React memproses render secara interruptible. Dua hook penting yang wajib kamu kuasai:
import { useState, useTransition, useDeferredValue } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const deferredQuery = useDeferredValue(query);
const handleChange = (e) => {
// Update input langsung (urgent)
setQuery(e.target.value);
// Update hasil pencarian bisa ditunda (non-urgent)
startTransition(() => {
// State update yang boleh diinterupsi React
});
};
return (
<div>
<input value={query} onChange={handleChange} placeholder="Cari produk..." />
{isPending && <span>Mencari...</span>}
{/* deferredQuery digunakan untuk render berat */}
<SearchResults query={deferredQuery} />
</div>
);
}
// Komponen hasil pencarian yang bisa menerima query yang ditunda
function SearchResults({ query }) {
// Simulasi filtering data berat
const results = query
? Array.from({ length: 100 }, (_, i) => `Hasil ${i + 1} untuk "${query}"`)
: [];
return (
<ul>
{results.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
export default SearchComponent;
Dengan useTransition, React bisa memprioritaskan update UI yang mendesak (seperti input pengguna) sambil menunda proses render yang berat.
Contoh Kode
1. Code Splitting dengan React.lazy dan Suspense
Salah satu cara paling efektif mengurangi initial load time adalah dengan memecah bundle menggunakan code splitting. Daripada memuat semua halaman sekaligus, kita muat hanya yang dibutuhkan.
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
// Komponen dimuat secara lazy (hanya saat dibutuhkan)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const ProductList = lazy(() => import('./pages/ProductList'));
const Analytics = lazy(() => import('./pages/Analytics'));
// Skeleton loader sebagai fallback
function PageSkeleton() {
return (
<div style={{ padding: '20px' }}>
<div style={{ height: '40px', background: '#eee', marginBottom: '16px', borderRadius: '4px' }} />
<div style={{ height: '200px', background: '#eee', borderRadius: '4px' }} />
</div>
);
}
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/dashboard">Dashboard</Link> |{' '}
<Link to="/products">Produk</Link> |{' '}
<Link to="/analytics">Analitik</Link>
</nav>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/products" element={<ProductList />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default App;
Dengan teknik ini, bundle utama bisa berkurang 40–60% untuk aplikasi skala besar.
2. Virtualisasi List Panjang dengan react-window
Jika kamu merender 10.000 item di DOM sekaligus, browser akan kewalahan. Solusinya adalah virtualisasi — hanya render item yang terlihat di viewport.
npm install react-window
import { FixedSizeList as List } from 'react-window';
// Data simulasi 10.000 produk
const products = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Produk ${i + 1}`,
price: Math.floor(Math.random() * 500000) + 10000,
}));
// Row renderer yang dioptimasi — menerima style dari react-window
const ProductRow = ({ index, style }) => {
const product = products[index];
return (
<div
style={{
...style,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 16px',
borderBottom: '1px solid #eee',
}}
>
<span>{product.name}</span>
<span style={{ color: '#e53e3e', fontWeight: 'bold' }}>
Rp {product.price.toLocaleString('id-ID')}
</span>
</div>
);
};
function ProductListVirtualized() {
return (
<div>
<h2>Daftar Produk (10.000 item)</h2>
<List
height={600} // Tinggi container (px)
itemCount={10000} // Total item
itemSize={60} // Tinggi setiap row (px)
width="100%"
>
{ProductRow}
</List>
</div>
);
}
export default ProductListVirtualized;
Hasilnya? Dari 10.000 DOM node menjadi hanya ~15 node yang aktif di viewport — performa meningkat drastis.
3. Menggunakan React Profiler untuk Diagnosa
Sebelum mengoptimasi, kamu harus tahu apa yang lambat. React Profiler API membantu mengidentifikasi komponen yang terlalu sering atau lama dirender.
import { Profiler, useState } from 'react';
// Callback dipanggil setiap kali komponen dirender
function onRenderCallback(
id, // ID Profiler
phase, // "mount" atau "update"
actualDuration, // Waktu render aktual (ms)
baseDuration, // Estimasi tanpa memoization (ms)
startTime,
commitTime
) {
if (actualDuration > 16) {
// Render > 16ms akan drop frame (60fps = 16ms per frame)
console.warn(`Render lambat di "${id}": ${actualDuration.toFixed(2)}ms (fase: ${phase})`);
}
}
// Komponen yang akan diprofile
function SlowComponent({ count }) {
// Simulasi komputasi berat
const items = Array.from({ length: count }, (_, i) => i * 2);
return (
<ul>
{items.map((item) => (
<li key={item}>Item {item}</li>
))}
</ul>
);
}
function App() {
const [count, setCount] = useState(100);
return (
<div>
<button onClick={() => setCount((c) => c + 100)}>Tambah Item</button>
<Profiler id="SlowComponent" onRender={onRenderCallback}>
<SlowComponent count={count} />
</Profiler>
</div>
);
}
export default App;
Selain Profiler API, gunakan juga React DevTools Profiler di browser untuk visualisasi yang lebih detail.
4. Optimasi dengan memo dan useCallback
React.memo mencegah re-render jika props tidak berubah. Namun, penggunaannya harus terukur — jangan memoize semua komponen karena ada overhead perbandingan props.
import { memo, useState, useCallback } from 'react';
// Helper untuk generate data produk
function generateProducts(count) {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
name: `Produk ${i + 1}`,
price: (i + 1) * 15000,
}));
}
// Komponen berat yang dioptimasi dengan memo
const ProductCard = memo(
({ product, onAddToCart }) => {
console.log(`Render ProductCard: ${product.id}`);
return (
<div style={{ border: '1px solid #ddd', padding: '16px', borderRadius: '8px' }}>
<h3>{product.name}</h3>
<p>Rp {product.price.toLocaleString('id-ID')}</p>
<button onClick={() => onAddToCart(product.id)}>
Tambah ke Keranjang
</button>
</div>
);
},
// Custom comparison: hanya re-render jika id atau price berubah
(prevProps, nextProps) =>
prevProps.product.id === nextProps.product.id &&
prevProps.product.price === nextProps.product.price
);
function ProductGrid() {
const [cart, setCart] = useState([]);
const [products] = useState(() => generateProducts(20));
// useCallback mencegah fungsi baru dibuat setiap render
const handleAddToCart = useCallback((productId) => {
setCart((prev) => [...prev, productId]);
}, []); // Dependensi kosong = fungsi stabil
return (
<div>
<p>Keranjang: {cart.length} item</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</div>
</div>
);
}
export default ProductGrid;
5. Mencegah Memory Leak dengan AbortController
Memory leak sering terjadi saat komponen unmount tapi masih ada subscription atau async operation yang berjalan. Ini seperti lupa menutup keran air — lambat laun memori penuh.
import { useState, useEffect, useRef } from 'react';
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
useEffect(() => {
// Buat AbortController baru setiap kali userId berubah
abortControllerRef.current = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ signal: abortControllerRef.current.signal }
);
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
const result = await response.json();
setData(result);
} catch (err) {
// Abaikan error abort (bukan error sebenarnya)
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
// Cleanup: batalkan request saat komponen unmount atau userId berubah
return () => {
abortControllerRef.current?.abort();
};
}, [userId]);
if (loading) return <div>Memuat data pengguna #{userId}...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h3>{data?.name}</h3>
<p>Email: {data?.email}</p>
<p>Telepon: {data?.phone}</p>
</div>
);
}
function App() {
const [userId, setUserId] = useState(1);
return (
<div>
<button onClick={() => setUserId((id) => Math.max(1, id - 1))}>Prev</button>
<span> User #{userId} </span>
<button onClick={() => setUserId((id) => Math.min(10, id + 1))}>Next</button>
<DataFetcher userId={userId} />
</div>
);
}
export default App;
6. Web Vitals Monitoring
Jika ingin membangun aplikasi sekelas marketplace besar, monitoring Core Web Vitals secara real-time adalah keharusan.
npm install web-vitals
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
// Threshold berdasarkan standar Google
const THRESHOLDS = {
CLS: 0.1, // Harus < 0.1
INP: 200, // Harus < 200ms
LCP: 2500, // Harus < 2500ms
FCP: 1800, // Harus < 1800ms
TTFB: 800, // Harus < 800ms
};
function sendToAnalytics({ name, value, id, rating }) {
const threshold = THRESHOLDS[name];
const status = value <= threshold ? 'BAIK' : 'PERLU DIPERBAIKI';
console.log(`[Web Vitals] ${name}: ${value.toFixed(2)} — ${status} (rating: ${rating})`);
// Kirim ke backend analytics menggunakan sendBeacon (tidak blocking)
if (navigator.sendBeacon) {
navigator.sendBeacon(
'/api/vitals',
JSON.stringify({
metric: name,
value: Math.round(name === 'CLS' ? value * 1000 : value),
id,
url: window.location.href,
rating,
})
);
}
}
// Pantau semua Core Web Vitals — panggil di entry point aplikasi
export function initWebVitals() {
onCLS(sendToAnalytics); // Cumulative Layout Shift
onINP(sendToAnalytics); // Interaction to Next Paint
onLCP(sendToAnalytics); // Largest Contentful Paint
onFCP(sendToAnalytics); // First Contentful Paint
onTTFB(sendToAnalytics); // Time to First Byte
}
Integrasikan initWebVitals() di entry point aplikasi (main.jsx atau index.js) untuk pemantauan berkelanjutan. Teknik ini juga relevan saat kamu men-deploy ke infrastruktur cloud, mirip seperti yang dibahas di cara mengatasi error 521 di Cloudflare.
7. Optimasi Bundle dengan Dynamic Import
Selain React.lazy, kamu bisa menggunakan dynamic import untuk library besar yang tidak selalu dibutuhkan.
import { useRef, useState } from 'react';
// Data penjualan simulasi
const salesData = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'],
datasets: [
{
label: 'Penjualan (juta Rp)',
data: [12, 19, 8, 25, 22, 35],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1,
},
],
};
async function loadAndRenderChart(data, canvasRef) {
// Chart.js hanya dimuat saat fungsi ini dipanggil — tidak masuk bundle utama
const { Chart, registerables } = await import('chart.js');
Chart.register(...registerables);
// Hapus chart lama jika ada
const existing = Chart.getChart(canvasRef.current);
if (existing) existing.destroy();
new Chart(canvasRef.current, {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Laporan Penjualan 2024' },
},
},
});
}
function AnalyticsPage() {
const canvasRef = useRef(null);
const [chartLoaded, setChartLoaded] = useState(false);
const [loading, setLoading] = useState(false);
const handleShowChart = async () => {
setLoading(true);
await loadAndRenderChart(salesData, canvasRef);
setChartLoaded(true);
setLoading(false);
};
return (
<div style={{ padding: '24px' }}>
<h2>Analitik Penjualan</h2>
{!chartLoaded && (
<button onClick={handleShowChart} disabled={loading}>
{loading ? 'Memuat grafik...' : 'Tampilkan Grafik Penjualan'}
</button>
)}
<canvas ref={canvasRef} style={{ maxHeight: '400px' }} />
</div>
);
}
export default AnalyticsPage;
Pola ini juga berguna saat bekerja dengan logika async kompleks menggunakan Arrow Function dan closures di JavaScript.
Troubleshooting: Error yang Sering Muncul
Komponen Terus Re-render Meski Menggunakan React.memo
Penyebab: Props yang dikirim adalah object atau array baru di setiap render parent, sehingga perbandingan referensi (===) selalu gagal meski nilainya sama secara nilai.
Solusi:
import { memo, useMemo } from 'react';
const Child = memo(({ config }) => {
console.log('Child render');
return <div>{config.theme}</div>;
});
// ❌ Masalah: object literal baru dibuat setiap kali Parent render
function ParentBuruk() {
return <Child config={{ theme: 'dark', size: 'lg' }} />;
}
// ✅ Solusi 1: pindahkan ke luar komponen jika nilainya statis
const STATIC_CONFIG = { theme: 'dark', size: 'lg' };
function ParentBaik1() {
return <Child config={STATIC_CONFIG} />;
}
// ✅ Solusi 2: gunakan useMemo untuk object yang bergantung pada props
function ParentBaik2({ userId }) {
const config = useMemo(
() => ({ theme: 'dark', userId }),
[userId] // Hanya dibuat ulang jika userId berubah
);
return <Child config={config} />;
}
Warning: “Can’t perform a React state update on an unmounted component”
Penyebab: Ada operasi async (fetch, setTimeout) yang mencoba memanggil setState setelah komponen sudah di-unmount dari DOM — biasanya karena cleanup useEffect tidak diimplementasikan.
Solusi:
import { useState, useEffect, useRef } from 'react';
function SafeAsyncComponent({ resourceId }) {
const [data, setData] = useState(null);
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
async function fetchResource() {
const result = await fetch(`/api/resource/${resourceId}`).then((r) =>
r.json()
);
// Cek apakah komponen masih mounted sebelum setState
if (isMountedRef.current) {
setData(result);
}
}
fetchResource();
// Cleanup: tandai sebagai unmounted saat komponen hilang dari DOM
return () => {
isMountedRef.current = false;
};
}, [resourceId]);
return <div>{data ? JSON.stringify(data) : 'Memuat...'}</div>;
}
export default SafeAsyncComponent;
Error: “Each child in a list should have a unique ‘key’ prop”
Penyebab: React membutuhkan prop key yang unik dan stabil untuk setiap elemen di dalam list agar bisa melacak perubahan secara efisien. Menggunakan index array sebagai key menyebabkan bug saat urutan list berubah.
Solusi:
const items = [
{ id: 'a1', name: 'Item Alpha' },
{ id: 'b2', name: 'Item Beta' },
{ id: 'c3', name: 'Item Gamma' },
];
// ❌ Buruk: index sebagai key menyebabkan bug saat sorting/filtering
function ListBuruk() {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
);
}
// ✅ Baik: gunakan ID unik yang stabil dari data
function ListBaik() {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Pertanyaan yang Sering Diajukan
Apakah virtualisasi list selalu diperlukan?
Tidak selalu. Untuk list di bawah 100–200 item, virtualisasi biasanya tidak diperlukan dan justru menambah kompleksitas kode. Mulai pertimbangkan virtualisasi ketika list memiliki 500+ item, setiap item memiliki layout kompleks, atau kamu mendapati jank saat scrolling yang terlihat di browser DevTools (FPS drop di bawah 60).
Bagaimana cara mengukur performa sebelum dan sesudah optimasi?
Gunakan React DevTools Profiler untuk mengukur durasi render tiap komponen. Di tab Performance Chrome DevTools, perhatikan metrik Frames Per Second (FPS) — target 60 FPS untuk pengalaman yang mulus. Selain itu, gunakan library web-vitals untuk mengukur Core Web Vitals (LCP, INP, CLS) yang juga mempengaruhi ranking SEO Google. Selalu ukur sebelum dan sesudah optimasi untuk memastikan ada peningkatan nyata.
Apakah Concurrent Mode mengubah cara menulis komponen React?
Sebagian besar kode React yang sudah ada tetap kompatibel dengan Concurrent Mode. Namun, kamu perlu memastikan efek di useEffect memiliki cleanup yang benar, karena React 18 bisa me-mount dan unmount komponen lebih dari satu kali dalam Strict Mode. Hal ini dilakukan untuk mendeteksi side effects yang tidak aman — jika aplikasimu berperilaku aneh di development tapi baik-baik saja di production, pastikan cleanup useEffect sudah diimplementasikan dengan benar.
Kapan sebaiknya menggunakan useMemo vs useCallback?
Gunakan useMemo untuk memoize nilai yang mahal dihitung ulang (array hasil filter, objek konfigurasi), dan gunakan useCallback untuk memoize fungsi yang dikirim sebagai props ke komponen child yang dibungkus memo. Jangan gunakan keduanya untuk semua hal — memoization sendiri memiliki overhead. Terapkan hanya setelah mengidentifikasi masalah performa yang nyata melalui Profiler.
Apa perbedaan antara React.lazy dan dynamic import biasa?
React.lazy khusus dirancang untuk memuat komponen React secara lazy dan harus digunakan bersama Suspense untuk menampilkan fallback saat loading. Dynamic import biasa (await import(...)) lebih fleksibel dan bisa digunakan untuk memuat library apa pun (Chart.js, lodash, dll.) secara on-demand, tidak hanya komponen React. Keduanya menggunakan mekanisme code splitting yang sama di baliknya.
Kesimpulan
Optimasi performa React bukan tentang menerapkan semua teknik sekaligus, melainkan tentang mengidentifikasi bottleneck yang tepat dan menggunakan solusi yang sesuai. Mulai dengan React Profiler untuk diagnosa, terapkan code splitting untuk mengurangi initial load, gunakan virtualisasi untuk list panjang, dan manfaatkan Concurrent Features di React 18 untuk UI yang lebih responsif.
Ingat prinsip dasarnya: ukur dulu, optimasi kemudian. Optimasi prematur hanya membuang waktu dan membuat kode lebih kompleks tanpa manfaat nyata. Jika ingin membangun aplikasi berskala besar seperti platform marketplace, teknik-teknik di atas adalah fondasi yang wajib dikuasai.
Selamat belajar dan terus bereksperimen — performa aplikasi yang baik bukan hanya tentang kecepatan teknis, tapi juga pengalaman pengguna yang lebih menyenangkan, dan itulah yang membuat developer React sejati bangga dengan karyanya! Jika ada pertanyaan atau kamu ingin mengeksplorasi topik lebih lanjut, jangan ragu untuk menjelajahi artikel lainnya di KamusNgoding.