Langsung ke konten
KamusNgoding
Mahir React 6 menit baca

Optimasi Performa Aplikasi React: Tips dan Trik Pro

#react #performance #code splitting #lazy loading #advanced

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:

  1. Re-render berlebihan — komponen dirender ulang meski datanya tidak berubah
  2. Bundle size terlalu besar — semua kode dimuat sekaligus di awal
  3. List panjang tanpa virtualisasi — merender 10.000 item sekaligus ke DOM
  4. Memory leak — efek samping yang tidak dibersihkan
  5. 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.

Artikel Terkait