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.
+45 poin Lighthouse dalam 2 hari kerja penuh. Semua teknik di bawah ini berkontribusi — tidak ada satu "magic bullet".
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:
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
# Install bundle analyzer npm install --save-dev @next/bundle-analyzer # Jalankan analisis bundle ANALYZE=true npm run build
01 Optimasi Gambar dengan next/image
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: 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: 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
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
Komponen seperti chart library, text editor, map, atau modal yang tidak langsung terlihat —
jangan di-bundle bersama initial load. Gunakan dynamic() dari Next.js.
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
Ini adalah kesalahan arsitektur paling mahal. Banyak developer menggunakan SSR (getServerSideProps) untuk semua halaman padahal tidak semua halaman butuh data fresh setiap request.
// ❌ 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
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
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.
// ❌ 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
JavaScript yang besar adalah pembunuh performa nomor satu. Parse dan execute JS memakan waktu CPU —
terutama di mobile. Audit bundle dengan @next/bundle-analyzer.
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
// ❌ 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 digantinext/imagedenganaltyang 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 builddan cek bundle > 100KB - Import library dengan named import atau path langsung
- Lighthouse mobile score > 90 sebelum merge ke main
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
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 →