Optimasi Core Web Vitals di Aplikasi React

I Nyoman Donostia 812 kata
Optimasi Core Web Vitals di Aplikasi React

Apa itu Core Web Vitals?

Core Web Vitals adalah metrik resmi Google untuk mengukur user experience halaman web:

  • LCP (Largest Contentful Paint): waktu render elemen terbesar — target < 2.5s
  • INP (Interaction to Next Paint): responsivitas interaksi — target < 200ms
  • CLS (Cumulative Layout Shift): stabilitas visual — target < 0.1

Skor ini memengaruhi SEO ranking. Dalam project e-commerce klien saya, setelah optimasi 2 minggu, organic traffic naik 35%.

1. Menganalisis Bundle Size

# Install analyzer webpack
npm install --save-dev webpack-bundle-analyzer

# Atau gunakan vite-bundle-visualizer untuk Vite
npm install --save-dev rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    visualizer({ open: true, filename: 'bundle-stats.html' }),
  ],
});

Setelah build, buka bundle-stats.html. Identifikasi chunks besar yang bisa di-split.

2. Code Splitting dengan React.lazy

Jangan import semua komponen sekaligus. Lazy load yang berat.

// 🚫 Semua dimuat di awal
import HeavyChart from './charts/HeavyChart';
import HeavyModal from './modals/HeavyModal';
import PdfViewer from './viewers/PdfViewer';

// ✅ Lazy load saat dibutuhkan
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./charts/HeavyChart'));
const HeavyModal = lazy(() => import('./modals/HeavyModal'));
const PdfViewer = lazy(() => import('./viewers/PdfViewer'));

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <HeavyChart />
    </Suspense>
  );
}

Bundle chunks terpisah hanya diunduh saat komponen pertama dipakai.

3. Tree Shaking yang Proper

// 🚫 Import seluruh library lodash
import _ from 'lodash';
_.debounce(fn, 300);

// ✅ Import hanya method yang dibutuhkan
import debounce from 'lodash/debounce';
debounce(fn, 300);

// Atau gunakan lodash-es yang tree-shakeable
import { debounce } from 'lodash-es';

Cek dengan ESLint rule no-restricted-imports.

// .eslintrc
{
  "rules": {
    "no-restricted-imports": [
      "error",
      {
        "paths": [{
          "name": "lodash",
          "message": "Import dari lodash spesifik method: import debounce from 'lodash/debounce'"
        }]
      }
    ]
  }
}

4. Optimasi Image yang Krusial untuk LCP

Gambar hero sering jadi elemen LCP. Optimasi langsung memengaruhi LCP.

// next/image atau komponen custom
import { useEffect, useState } from 'react';

function OptimizedImage({ src, alt, priority }: { src: string; alt: string; priority?: boolean }) {
  return (
    <img
      src={src}
      alt={alt}
      loading={priority ? 'eager' : 'lazy'}
      decoding="async"
      sizes="(max-width: 768px) 100vw, 50vw"
      style={{ width: '100%', height: 'auto' }}
    />
  );
}

// Hero image (LCP)
<OptimizedImage src="/hero.jpg" priority alt="Hero" />

// Below-fold
<OptimizedImage src="/feature.jpg" alt="Feature" />

Tips format gambar:

  • Gunakan AVIF atau WebP (90% browsers support)
  • Hapus EXIF metadata: imagemagick / sharp
  • Sediakan thumbnail LQIP (Low Quality Image Placeholder) untuk blur-up effect

5. Mengurangi CLS (Layout Shift)

Penyebab CLS tinggi:

  1. Gambar tanpa dimensi → browser tidak tahu ukuran sebelum load
  2. Font web yang menggantikan font fallback
  3. Banner/konten yang inject setelah load
// ✅ Selalu set aspect ratio untuk gambar
<div className="aspect-video">
  <img src="/video-thumbnail.jpg" className="w-full h-full object-cover" />
</div>

// ✅ Cadangkan ruang font
import { Inter } from 'next/font/google';
const inter = Inter({ 
  subsets: ['latin'],
  display: 'swap',
  fallback: ['system-ui', 'sans-serif'],
});

6. Memoization untuk Mengurangi Re-render

import { memo, useMemo, useCallback } from 'react';

// Pure component tidak perlu re-render kalau props sama
const UserCard = memo(function UserCard({ user, onClick }: Props) {
  return (
    <div onClick={onClick}>
      <h3>{user.name}</h3>
    </div>
  );
});

// Stable callbacks
function UserList({ users }: Props) {
  const [filter, setFilter] = useState('');
  
  const handleClick = useCallback((id: string) => {
    // Logic stable
  }, []);
  
  // Filter hanya recompute kalau users atau filter berubah
  const filtered = useMemo(() =>
    users.filter(u => u.name.includes(filter)),
    [users, filter]
  );
  
  return filtered.map(u => <UserCard key={u.id} user={u} onClick={handleClick} />);
}

7. Virtualisasi List Panjang

// react-window untuk list ribuan item
import { FixedSizeList } from 'react-window';

function BigList({ items }: { items: Item[] }) {
  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={80}
    >
      {({ index, style }) => (
        <div style={style}>
          <ItemCard item={items[index]} />
        </div>
      )}
    </FixedSizeList>
  );
}

Hanya baris yang terlihat yang dirender. Memory footprint constant.

8. Service Worker untuk Caching

// next-pwa atau workbox custom
import withPWAInit from 'next-pwa';

const withPWA = withPWAInit({
  dest: 'public',
  cacheOnFrontEndNav: true,
  aggressiveFrontEndNavCaching: true,
  runtimeCaching: [
    {
      urlPattern: /^https?.*\.(?:png|jpg|jpeg|webp|avif)$/,
      handler: 'CacheFirst',
      options: { cacheName: 'images', expiration: { maxEntries: 100 } },
    },
  ],
});

9. Monitoring Real User Metrics (RUM)

// web-vitals library dari Google
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(metric => sendToAnalytics('lcp', metric.value));
onINP(metric => sendToAnalytics('inp', metric.value));
onCLS(metric => sendToAnalytics('cls', metric.value));

function sendToAnalytics(metric: string, value: number) {
  navigator.sendBeacon('/api/metrics', JSON.stringify({ metric, value, page: location.pathname }));
}

Kirim ke analytics untuk memantau score real user, bukan hanya Lighthouse local.

Kesimpulan

Optimasi Core Web Vitals itu iterative:

  1. Analyze: bundle analyzer + Lighthouse + RUM
  2. Optimize: code splitting, image format, memoization
  3. Verify: rollback improvements, measure real users
  4. Maintain: guardrails di CI (bundle size check, perf budget)

Investasi 1-2 minggu optimasi bisa naik 20-40% organic traffic — angka yang signifikan untuk ROI bisnis.

I Nyoman Donostia

I Nyoman Donostia

Fullstack Developer - Bali, Indonesia

Artikel Terkait