Beberapa bulan lalu, saya diminta mengaudit sebuah website e-commerce yang sudah berjalan. Hasilnya mengejutkan: loading time 3.2 detik, Lighthouse Performance score 52, dan bounce rate 68%. Setelah dua hari kerja menerapkan teknik-teknik berikut, angkanya berubah drastis.

3.2s
LCP Sebelum
0.8s
LCP Sesudah
75%
Lebih Cepat
52
Sebelum
97
Sesudah

+45 poin Lighthouse dalam 2 hari kerja penuh. Semua teknik di bawah ini berkontribusi — tidak ada satu "magic bullet".

💡 Tools yang digunakan dalam artikel ini: Chrome DevTools (Lighthouse), next/bundle-analyzer, WebPageTest.org, dan React DevTools Profiler.

00 Diagnosis: Temukan Bottleneck Dulu

Sebelum optimasi, wajib diagnosis dulu. Jangan asal optimasi tanpa data — buang-buang waktu dan bisa salah sasaran. Gunakan tiga tools ini secara berurutan:

1 Jalankan Lighthouse Audit Wajib Pertama

Buka Chrome DevTools → Lighthouse → pilih Mobile (lebih representatif dari Desktop) → Generate report. Perhatikan empat metrik Core Web Vitals:

  • LCP (Largest Contentful Paint) — target < 2.5s
  • INP (Interaction to Next Paint) — target < 200ms
  • CLS (Cumulative Layout Shift) — target < 0.1
  • FCP (First Contentful Paint) — target < 1.8s
BASH Install bundle analyzer
# Install bundle analyzer
npm install --save-dev @next/bundle-analyzer

# Jalankan analisis bundle
ANALYZE=true npm run build

01 Optimasi Gambar dengan next/image

01 Ganti semua <img> dengan next/image Impact Tinggi
⚡ Saving: 800KB–2MB per halaman

Ini adalah optimasi dengan impact paling besar dan paling sering diabaikan. Komponen next/image otomatis melakukan: konversi ke WebP/AVIF, lazy loading, dan resize sesuai viewport.

❌ Sebelum — img biasa
// ❌ SEBELUM: 1.2MB JPG, no lazy load
<img
  src="/hero-banner.jpg"
  alt="Hero Banner"
  width="1200"
  height="600"
/>
// Load: 1.2MB · No WebP · No lazy
✅ Sesudah — next/image
// ✅ SESUDAH: Auto WebP, lazy, resize
import Image from 'next/image';

<Image
  src="/hero-banner.jpg"
  alt="Hero Banner"
  width={1200}
  height={600}
  priority  // LCP image: eager load
  placeholder="blur"
/>
// Load: 68KB WebP · Lazy · Responsive
⚠️ Penting: Tambahkan priority prop HANYA untuk gambar yang terlihat di atas fold (above-the-fold). Gambar ini adalah LCP element — jangan di-lazy-load. Untuk gambar di bawah fold, biarkan default (lazy).

02 Dynamic Import & Code Splitting

02 Lazy load komponen berat dengan dynamic() Impact Tinggi
⚡ Saving: 200–600KB dari initial bundle

Komponen seperti chart library, text editor, map, atau modal yang tidak langsung terlihat — jangan di-bundle bersama initial load. Gunakan dynamic() dari Next.js.

JSX pages/dashboard.jsx
import dynamic from 'next/dynamic';

// ❌ SEBELUM: Bundle recharts masuk ke initial load (~350KB)
// import { LineChart, BarChart } from 'recharts';

// ✅ SESUDAH: Load hanya saat komponen dibutuhkan
const RevenueChart = dynamic(
  () => import('../components/RevenueChart'),
  {
    loading: () => <ChartSkeleton />,  // Skeleton saat loading
    ssr: false,               // Tidak perlu SSR untuk chart
  }
);

// ✅ Modal: Load hanya saat dibuka
const EditModal = dynamic(
  () => import('../components/EditModal'),
  { loading: () => null }
);

export default function Dashboard() {
  const [showModal, setShowModal] = useState(false);
  return (
    <div>
      <RevenueChart />   // Lazy loaded
      {showModal && <EditModal />}
    </div>
  );
}

03 Caching Strategy: ISR & SWR

03 Pilih rendering strategy yang tepat Impact Tinggi
⚡ Saving: Server response 400ms → 8ms

Ini adalah kesalahan arsitektur paling mahal. Banyak developer menggunakan SSR (getServerSideProps) untuk semua halaman padahal tidak semua halaman butuh data fresh setiap request.

JSX pages/products/index.jsx — ISR
// ❌ SSR: Fetch ulang setiap request → lambat
export async function getServerSideProps() {
  const products = await fetchProducts(); // ~400ms tiap visit
  return { props: { products } };
}

// ✅ ISR: Build sekali, revalidate tiap 60 detik
export async function getStaticProps() {
  const products = await fetchProducts();
  return {
    props: { products },
    revalidate: 60, // ✅ Regenerate tiap 60 detik
  };                // Response: 8ms dari CDN cache
}

// ✅ App Router (Next.js 14+): fetch dengan cache config
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 } // ISR equivalent di App Router
  });
  return res.json();
}

Kapan pakai SSG, ISR, SSR, atau CSR?

  • SSG — Data statis, tidak berubah: halaman About, Privacy Policy
  • ISR — Data berubah berkala: blog, product listing, homepage promo
  • SSR — Data real-time per-user: cart, dashboard personal, auth pages
  • CSR (SWR/React Query) — Data interaktif: search, filter, live update
JSX hooks/useProducts.js — SWR
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then(r => r.json());

export function useProducts(category) {
  const { data, error, isLoading } = useSWR(
    `/api/products?cat=${category}`,
    fetcher,
    {
      revalidateOnFocus: false,   // Jangan refetch saat tab focus
      dedupingInterval: 30000,     // Cache 30 detik
      fallbackData: [],
    }
  );
  return { products: data, error, isLoading };
}

04 Font Optimization yang Benar

04 Pakai next/font — bukan link Google Fonts biasa Impact Menengah
⚡ Saving: Eliminasi 300–500ms render-blocking request

Loading font dari Google Fonts via <link> di HTML menyebabkan render-blocking request. next/font mendownload dan self-host font saat build time — zero layout shift, zero network request.

JSX app/layout.jsx — next/font
// ❌ SEBELUM: Link Google Fonts — render blocking!
// <link href="https://fonts.googleapis.com/css2?...

// ✅ SESUDAH: next/font — zero network request
import { Inter, Fraunces } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',       // Tampilkan fallback font dulu
  preload: true,
});

const fraunces = Fraunces({
  subsets: ['latin'],
  variable: '--font-fraunces',
  weight: ['700', '900'],   // Hanya weight yang dipakai
  display: 'swap',
});

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${fraunces.variable}`}>
      <body>{children}</body>
    </html>
  );
}

05 Bundle Analysis & Tree Shaking

05 Analisis dan kurangi ukuran JavaScript bundle Impact Tinggi
⚡ Saving: 300KB–1MB dari bundle size

JavaScript yang besar adalah pembunuh performa nomor satu. Parse dan execute JS memakan waktu CPU — terutama di mobile. Audit bundle dengan @next/bundle-analyzer.

JS next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

const nextConfig = {
  images: {
    formats: ['image/avif', 'image/webp'], // Prioritas AVIF
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 hari cache
  },
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
  // Minify agresif di production
  swcMinify: true,
};

module.exports = withBundleAnalyzer(nextConfig);

3 Library Penyebab Bundle Besar yang Sering Lolos

JSX Import yang benar
// ❌ SALAH: Import seluruh library lodash (71KB!)
import _ from 'lodash';
const result = _.debounce(fn, 300);

// ✅ BENAR: Import hanya yang dipakai (3KB)
import debounce from 'lodash/debounce';

// ❌ SALAH: Seluruh date-fns (80KB)
import * as dateFns from 'date-fns';

// ✅ BENAR: Named import, tree-shakeable (4KB)
import { format, parseISO } from 'date-fns';

// ❌ SALAH: Seluruh MUI Icons (2MB!)
import { Home, Settings } from '@mui/icons-material';

// ✅ BENAR: Import langsung per icon (2KB)
import HomeIcon     from '@mui/icons-material/Home';
import SettingsIcon from '@mui/icons-material/Settings';

✅ Checklist Optimasi Next.js — Sebelum Deploy

  • Semua <img> sudah diganti next/image dengan alt yang benar
  • Gambar above-the-fold punya prop priority
  • Komponen berat (chart, map, editor) menggunakan dynamic()
  • Halaman product/blog menggunakan ISR bukan SSR
  • Font dimuat via next/font — tidak ada Google Fonts <link>
  • Jalankan ANALYZE=true npm run build dan cek bundle > 100KB
  • Import library dengan named import atau path langsung
  • Lighthouse mobile score > 90 sebelum merge ke main
⚡ Key Takeaway

Performa bukan fitur tambahan — ini adalah fondasi UX dan SEO. Google menggunakan Core Web Vitals sebagai ranking factor. Website yang lambat langsung kehilangan posisi di SERP dan kehilangan konversi. Lima teknik di artikel ini cukup untuk membawa hampir semua Next.js app ke Lighthouse 90+ — saya sudah buktikan di project nyata.

❓ Pertanyaan yang Sering Ditanya

Berapa Lighthouse score ideal untuk website Next.js?
Target minimal adalah 90+ untuk semua kategori (Performance, Accessibility, Best Practices, SEO). Dengan optimasi yang tepat, score 95–100 sangat achievable di Next.js. Ukur di mobile karena lebih representatif dari kondisi real user.
Apakah ISR cocok untuk semua halaman?
Tidak semua. ISR cocok untuk halaman yang datanya jarang berubah (blog, product listing, homepage). Untuk data real-time atau per-user, gunakan SSR atau Client-side fetching dengan SWR. Gunakan SSR hanya ketika benar-benar dibutuhkan karena setiap request membebani server.
Apa penyebab utama Next.js loading lambat?
Lima penyebab paling umum: (1) Gambar tidak dioptimasi (tanpa next/image), (2) tidak menggunakan dynamic import, (3) tidak ada caching strategy yang tepat, (4) bundle JavaScript terlalu besar karena import yang salah, dan (5) font loading yang render-blocking.
Apakah semua teknik ini berlaku untuk Next.js 14 App Router?
Ya, semua teknik relevan. Di App Router, getStaticProps diganti dengan fetch + { next: { revalidate: N } }. next/image, next/font, dan dynamic import tetap sama. Bundle analyzer juga tetap bekerja.

Kalau kamu mau belajar Next.js lebih dalam — termasuk performa, deployment, dan arsitektur yang scalable — saya buka kelas privat dan bootcamp. Semua materi dari project production nyata, bukan dari tutorial YouTube.

🎓 Daftar Kelas Next.js →