Optimasi Render UI dengan Virtual Proxy Pattern di React
Pendahuluan
Pernahkah kamu membuka aplikasi web yang terasa berat saat pertama kali dimuat? Atau melihat daftar ribuan item yang membuat browser terasa seperti kehabisan napas? Masalah ini bukan soal koneksi internet yang lambat — sering kali akarnya ada di cara komponen React merender terlalu banyak elemen sekaligus.
Bayangkan kamu sedang membangun aplikasi seperti Tokopedia yang menampilkan ribuan produk dalam satu halaman pencarian. Jika semua komponen dirender sekaligus, performa aplikasi akan anjlok drastis. Di sinilah Virtual Proxy Pattern hadir sebagai solusi elegan.
Artikel ini akan membedah cara kerja Virtual Proxy Pattern, implementasinya di React menggunakan React.lazy dan Suspense, serta teknik virtualisasi daftar panjang yang dipakai di aplikasi skala produksi.
Memahami Render yang Mahal di React
Sebelum masuk ke solusi, kita perlu memahami akar masalahnya. React bekerja dengan cara merender komponen ke dalam Virtual DOM lalu mensinkronkannya ke DOM nyata. Proses ini punya biaya komputasi — semakin banyak komponen yang dirender, semakin berat beban browser.
Ada dua jenis “render mahal” yang paling sering ditemui:
1. Initial Load yang Berat Semua JavaScript untuk seluruh aplikasi diunduh dan dieksekusi sekaligus saat halaman pertama kali dibuka. Ini disebut masalah bundle size yang besar.
2. Render Daftar Panjang Jika kamu merender array berisi 10.000 item, React akan membuat 10.000 DOM node sekaligus — bahkan yang tidak terlihat di layar.
Untuk memahami kenapa ini bermasalah dari sisi algoritma, bayangkan kompleksitas O(n) di mana n adalah jumlah item yang dirender. Semakin besar n, semakin lambat performa — persis seperti yang dijelaskan dalam konsep Mengenal Big O Notation: Cara Mengukur Efisiensi Algoritma.
Apa itu Virtual Proxy Pattern?
Proxy Pattern adalah design pattern struktural yang menempatkan sebuah “perantara” di antara klien dan objek asli. Proxy ini bisa mengontrol akses, menambah logika, atau menunda pembuatan objek yang mahal.
Virtual Proxy secara khusus menunda inisialisasi objek yang berat (expensive object) hingga benar-benar dibutuhkan. Prinsipnya sederhana: jangan buat sesuatu sampai kamu benar-benar butuh.
┌────────┐ request ┌─────────────┐ lazy load ┌──────────────┐
│ Client │ ─────────────▶│ Virtual │ ───────────────▶│ Real Object │
│ │ │ Proxy │ (only when │ (heavy comp) │
└────────┘ └─────────────┘ needed) └──────────────┘
Di konteks React, Virtual Proxy Pattern diterapkan melalui dua mekanisme utama:
- Code Splitting +
React.lazy: Komponen hanya diunduh saat pertama kali dirender - List Virtualization: Hanya item yang terlihat di viewport yang dirender ke DOM
Implementasi Virtual Proxy di React dengan React.lazy dan Suspense
Code Splitting dengan React.lazy
React.lazy adalah implementasi Virtual Proxy bawaan React. Komponen berat tidak diunduh sampai benar-benar diperlukan.
// Tanpa Virtual Proxy — semua diunduh saat aplikasi pertama kali dimuat
import HeavyDashboard from './HeavyDashboard';
import HeavyChart from './HeavyChart';
function App() {
return (
<div>
<HeavyDashboard />
<HeavyChart />
</div>
);
}
// Dengan Virtual Proxy (React.lazy) — hanya diunduh saat dibutuhkan
import React, { lazy, Suspense, useState } from 'react';
// Proxy: komponen hanya diunduh saat pertama kali dirender
const HeavyDashboard = lazy(() => import('./HeavyDashboard'));
const HeavyChart = lazy(() => import('./HeavyChart'));
function App() {
const [showChart, setShowChart] = useState(false);
return (
<div>
{/* Suspense menampilkan fallback selama komponen diunduh */}
<Suspense fallback={<div>Memuat dashboard...</div>}>
<HeavyDashboard />
</Suspense>
<button onClick={() => setShowChart(true)}>
Tampilkan Grafik
</button>
{/* HeavyChart hanya diunduh ketika showChart === true */}
{showChart && (
<Suspense fallback={<div>Memuat grafik...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
}
export default App;
Membuat Custom Virtual Proxy Component
Kita bisa membuat abstraksi proxy yang lebih fleksibel menggunakan Menguasai Fungsi dan Arrow Function di JavaScript sebagai dasar penulisan callback yang ringkas:
import React, { lazy, Suspense, useRef, useEffect, useState } from 'react';
// Higher-order component sebagai Virtual Proxy
function withVirtualProxy(importFn, fallback = <div>Memuat...</div>) {
const LazyComponent = lazy(importFn);
return function VirtualProxy(props) {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect(); // Stop observing setelah terlihat
}
},
{ threshold: 0.1 } // Trigger saat 10% komponen terlihat
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref} style={{ minHeight: '200px' }}>
{isVisible ? (
<Suspense fallback={fallback}>
<LazyComponent {...props} />
</Suspense>
) : (
fallback
)}
</div>
);
};
}
// Penggunaan
const LazyProductCard = withVirtualProxy(
() => import('./ProductCard'),
<div className="skeleton-loader">Memuat produk...</div>
);
function ProductList({ products }) {
return (
<div>
{products.map((product) => (
<LazyProductCard key={product.id} product={product} />
))}
</div>
);
}
export default ProductList;
Contoh Kasus Nyata: Virtualisasi Daftar Panjang
Jika ingin membangun fitur pencarian seperti di Shopee atau Bukalapak yang menampilkan ribuan produk, teknik list virtualization adalah kuncinya. Ide dasarnya: hanya render item yang berada di dalam viewport pengguna.
Implementasi Manual Window Virtualization
import React, { useState, useCallback, useRef } from 'react';
const ITEM_HEIGHT = 80; // Tinggi setiap item dalam pixel
const VISIBLE_COUNT = 10; // Jumlah item yang terlihat sekaligus
const BUFFER = 3; // Item tambahan di atas dan bawah viewport
function VirtualList({ items }) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);
const containerHeight = VISIBLE_COUNT * ITEM_HEIGHT;
const totalHeight = items.length * ITEM_HEIGHT;
// Hitung indeks item yang harus dirender
const startIndex = Math.max(
0,
Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER
);
const endIndex = Math.min(
items.length - 1,
Math.floor((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER
);
// Hanya item dalam rentang ini yang dirender — Virtual Proxy!
const visibleItems = items.slice(startIndex, endIndex + 1);
const handleScroll = useCallback((e) => {
setScrollTop(e.target.scrollTop);
}, []);
return (
<div
ref={containerRef}
style={{
height: containerHeight,
overflow: 'auto',
border: '1px solid #30363D',
borderRadius: '8px',
}}
onScroll={handleScroll}
>
{/* Container dengan tinggi total untuk scroll yang benar */}
<div style={{ height: totalHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={item.id}
style={{
position: 'absolute',
top: (startIndex + index) * ITEM_HEIGHT,
left: 0,
right: 0,
height: ITEM_HEIGHT,
display: 'flex',
alignItems: 'center',
padding: '0 16px',
borderBottom: '1px solid #30363D',
backgroundColor: '#161B22',
}}
>
<span style={{ fontWeight: 'bold', color: '#FFFFFF' }}>
{item.name}
</span>
<span style={{ marginLeft: 'auto', color: '#8B949E' }}>
Rp {item.price.toLocaleString('id-ID')}
</span>
</div>
))}
</div>
</div>
);
}
// Simulasi data 10.000 produk
const dummyProducts = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `Produk ${i + 1}`,
price: Math.floor(Math.random() * 1000000) + 10000,
}));
export default function App() {
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2 style={{ color: '#00FF88' }}>Daftar Produk (10.000 item)</h2>
<p style={{ color: '#8B949E', marginBottom: '16px' }}>
Hanya ~13–17 item yang dirender ke DOM pada satu waktu!
</p>
<VirtualList items={dummyProducts} />
</div>
);
}
Output yang dihasilkan:
Daftar Produk (10.000 item)
Hanya ~13–17 item yang dirender ke DOM pada satu waktu!
┌─────────────────────────────────────────────────┐
│ Produk 1 Rp 523.000 │
│ Produk 2 Rp 87.500 │
│ Produk 3 Rp 1.200.000 │
│ Produk 4 Rp 345.000 │
│ ... │
│ Produk 10 Rp 760.000 │
└─────────────────────────────────────────────────┘
[scroll untuk melihat lebih banyak...]
Dengan pendekatan ini, meski ada 10.000 item, browser hanya perlu merender sekitar 13–17 DOM node sekaligus — performa tetap mulus tanpa jeda.
Perbandingan Performa: Sebelum vs Sesudah
Untuk memahami dampak nyata Virtual Proxy Pattern, perhatikan perbandingan berikut:
| Metrik | Tanpa Virtualisasi | Dengan Virtualisasi |
|---|---|---|
| DOM node untuk 10.000 item | 10.000 node | ~13–17 node |
| Waktu render awal | ~2–5 detik | < 100ms |
| Penggunaan memori | Tinggi (semua di DOM) | Rendah (hanya visible) |
| Scroll FPS | < 30 FPS (patah-patah) | 60 FPS (mulus) |
| Bundle size (tanpa lazy) | Semua diunduh di awal | Hanya yang dibutuhkan |
Perbedaan ini sangat terasa di perangkat mobile entry-level yang banyak digunakan pengguna Indonesia.
Troubleshooting: Error yang Sering Muncul
A React component suspended while rendering, but no fallback UI was specified
Penyebab: Kamu menggunakan React.lazy tanpa membungkusnya dengan komponen <Suspense>. React membutuhkan fallback UI untuk ditampilkan selama proses lazy loading berlangsung.
Solusi:
// ❌ Salah — tanpa Suspense
function App() {
return <LazyComponent />;
}
// ✅ Benar — selalu bungkus dengan Suspense
function App() {
return (
<Suspense fallback={<div>Memuat...</div>}>
<LazyComponent />
</Suspense>
);
}
React.lazy hanya mendukung default export
Penyebab: React.lazy hanya bisa digunakan dengan komponen yang di-export sebagai default export. Named export tidak didukung secara langsung.
Solusi:
// ❌ Tidak bisa langsung dengan named export
const { MyComponent } = lazy(() => import('./components'));
// ✅ Buat wrapper untuk named export
const MyComponent = lazy(() =>
import('./components').then((module) => ({
default: module.MyComponent,
}))
);
List Virtualization: Item tampil di posisi yang salah
Penyebab: Tinggi item tidak konsisten atau tidak dihitung dengan benar, menyebabkan posisi absolute tidak akurat.
Solusi:
// Gunakan ResizeObserver untuk mengukur tinggi item secara dinamis
function useDynamicItemHeight() {
const heightsRef = useRef(new Map());
const measureRef = useCallback((index) => (node) => {
if (node) {
const height = node.getBoundingClientRect().height;
heightsRef.current.set(index, height);
}
}, []);
const getItemOffset = useCallback((index) => {
let offset = 0;
for (let i = 0; i < index; i++) {
offset += heightsRef.current.get(i) || 80; // Default 80px
}
return offset;
}, []);
return { measureRef, getItemOffset };
}
Komponen lazy tidak di-cache, diunduh ulang setiap render
Penyebab: Fungsi lazy(() => import(...)) didefinisikan di dalam komponen, sehingga membuat instance baru setiap render dan kehilangan cache.
Solusi:
// ❌ Salah — didefinisikan di dalam komponen
function App() {
// Re-create setiap render = download ulang setiap kali!
const LazyComp = lazy(() => import('./Heavy'));
return <Suspense fallback={null}><LazyComp /></Suspense>;
}
// ✅ Benar — definisikan di luar komponen (module level)
const LazyComp = lazy(() => import('./Heavy'));
function App() {
return <Suspense fallback={null}><LazyComp /></Suspense>;
}
Pertanyaan yang Sering Diajukan
Apa perbedaan Virtual Proxy Pattern dengan Decorator Pattern di React?
Virtual Proxy berfokus pada penundaan pembuatan objek/komponen yang berat hingga benar-benar dibutuhkan. Sementara Decorator Pattern berfokus pada penambahan perilaku ke komponen yang sudah ada tanpa mengubah strukturnya. Keduanya adalah design pattern struktural, tapi tujuannya berbeda — proxy untuk kontrol akses dan lazy loading, decorator untuk ekstensi fungsionalitas. Kamu bisa mempelajari lebih lanjut di artikel Apa Itu Decorator Pattern.
Bagaimana cara menentukan kapan harus menggunakan React.lazy vs library virtualisasi seperti react-window?
Gunakan React.lazy ketika masalahnya adalah ukuran bundle yang besar — komponen tertentu tidak perlu diunduh di awal. Gunakan library virtualisasi seperti react-window atau react-virtual ketika masalahnya adalah jumlah item yang sangat banyak dalam satu daftar. Keduanya bisa dikombinasikan untuk hasil optimal: lazy load komponen list-nya, lalu virtualisasi isinya.
Apakah Virtual Proxy Pattern memengaruhi SEO?
Untuk konten yang diunduh secara lazy, web crawler Google modern sudah bisa mengeksekusi JavaScript dan mengindeks konten. Namun, untuk konten yang sangat kritis untuk SEO, pertimbangkan untuk menggunakan Server-Side Rendering (SSR) atau Static Site Generation (SSG) agar konten tersedia langsung di HTML awal tanpa menunggu JavaScript dieksekusi.
Bagaimana cara mengukur efektivitas optimasi yang sudah diterapkan?
Gunakan React DevTools Profiler untuk mengukur waktu render komponen sebelum dan sesudah optimasi. Kamu juga bisa menggunakan Lighthouse di Chrome DevTools untuk mengukur metrik performa seperti LCP (Largest Contentful Paint) dan TTI (Time to Interactive). Perubahan yang signifikan pada metrik ini menandakan optimasi berhasil — target LCP di bawah 2,5 detik sudah dianggap baik oleh Google.
Apakah useMemo dan useCallback termasuk Virtual Proxy Pattern?
Secara teknis, useMemo dan useCallback lebih dekat ke Memoization Pattern — mereka menyimpan hasil komputasi dan hanya menghitung ulang jika dependensi berubah. Ini berbeda dari Virtual Proxy yang menunda pembuatan objek. Namun keduanya sama-sama bertujuan mengurangi pekerjaan komputasi yang tidak perlu di React dan bisa digunakan bersama untuk hasil terbaik.
Kesimpulan
Virtual Proxy Pattern adalah senjata ampuh untuk mengoptimasi performa aplikasi React, terutama saat menghadapi komponen berat atau daftar data yang sangat panjang. Dengan memanfaatkan React.lazy, Suspense, dan teknik virtualisasi daftar, kita bisa memastikan aplikasi tetap responsif meski menangani data dalam skala besar — seperti yang dibutuhkan saat membangun platform marketplace atau dashboard analitik.
Kunci utamanya adalah prinsip “jangan buat sesuatu sampai benar-benar dibutuhkan” — sebuah filosofi sederhana namun berdampak besar pada pengalaman pengguna. Selamat belajar dan terus eksplorasi teknik optimasi lainnya! Jika ada pertanyaan atau ingin mendalami topik terkait, jangan ragu untuk menjelajahi artikel-artikel lainnya di KamusNgoding — kamu selalu bisa kembali ke sini kapan pun butuh referensi!