Next.js Performance: Core Web Vitals & Beyond

Foto oleh Unsplash

Foto oleh Unsplash
Optimasi performa Next.js lebih dari sekadar nice-to-have — Core Web Vitals adalah faktor peringkat Google, dan aplikasi yang lambat kehilangan pengguna pada tingkat yang terukur. Postingan ini mencakup tumpukan penuh performa Next.js: optimasi gambar dan font, React Server Component untuk pengurangan JavaScript, strategi caching, analisis bundle, dan streaming dengan Suspense. Setiap teknik dilengkapi kode TypeScript yang dapat langsung Anda terapkan ke aplikasi Next.js 15 Anda.
Gambar dan font adalah dua sumber paling umum dari skor Largest Contentful Paint (LCP) dan Cumulative Layout Shift (CLS) yang buruk. Next.js menyediakan komponen bawaan untuk keduanya yang menangani bagian yang sulit secara otomatis — konversi format, hint ukuran, lazy loading, dan subsetting font.
Komponen next/image secara otomatis menyajikan format WebP atau AVIF (30-50% lebih kecil dari JPEG), menerapkan width dan height eksplisit untuk mencegah layout shift, dan lazy-load gambar di luar layar. Prop priority pada gambar LCP memicu preload link tag, mengurangi time-to-LCP. Prop sizes memberi tahu browser ukuran gambar mana yang harus diunduh pada setiap lebar viewport, mencegah gambar ukuran desktop di mobile.
// Next.js Image optimization — always use next/image
import Image from "next/image";
// Bad: raw <img> tag, no optimization
<img src="/hero.jpg" alt="Hero" />
// Good: automatic WebP/AVIF, lazy loading, size hints
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // LCP image: preload it
sizes="(max-width: 768px) 100vw, 1200px"
quality={85}
/>
// Font optimization with next/font (zero layout shift)
import { Inter } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
// next.config.ts — bundle analyzer
import { withNextBundleAnalyzer } from "@next/bundle-analyzer";
export default withNextBundleAnalyzer({ enabled: process.env.ANALYZE === "true" })({
// ... rest of config
});Gunakan Lighthouse CI dalam pipeline GitHub Actions Anda untuk melacak Core Web Vitals pada setiap pull request. Regresi performa yang ditangkap sebelum merge gratis untuk diperbaiki; yang ditangkap di produksi menghabiskan kepercayaan pengguna dan peringkat Google.
React Server Component (RSC) di-render di server dan mengirimkan HTML ke client — tidak ada JavaScript yang dikirimkan untuk komponen itu sendiri. Dalam aplikasi Next.js 15 yang khas, menggunakan RSC untuk pengambilan data dan rendering menghilangkan kilobyte JavaScript yang seharusnya membengkakkan client bundle. Code splitting melalui dynamic import selanjutnya mengurangi beban awal dengan menunda bundle komponen yang tidak kritis.
Default di Next.js App Router adalah Server Component — mereka dapat mengambil data secara langsung, mengakses resource sisi server, dan menghasilkan HTML tanpa mengirimkan JavaScript. Tambahkan 'use client' hanya ketika Anda membutuhkan browser API (useState, useEffect, event handler, WebSocket). Komposisi optimal adalah shell Server Component yang meneruskan data ke Client Component kecil dan terarah hanya di mana interaktivitas diperlukan.
// React Server Components: zero JS on the client by default
// app/blog/[slug]/page.tsx — Server Component
export default async function BlogPost({ params }: { params: { slug: string } }) {
// This fetch runs on the server — no client bundle cost
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 }, // ISR: revalidate every hour
}).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<PostContent content={post.content} /> {/* Server Component */}
<LikeButton postId={post.id} /> {/* Client Component — "use client" */}
</article>
);
}
// Streaming with Suspense for TTFB improvement
import { Suspense } from "react";
export default function Page() {
return (
<>
<HeroSection /> {/* Renders immediately */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* Streamed when ready */}
</Suspense>
</>
);
}Gunakan next/dynamic untuk menunda library pihak ketiga yang besar dan komponen berat sampai diperlukan. Rich text editor, charting library, atau komponen peta mungkin menambah 200KB ke bundle awal Anda. Dengan dynamic import dan ssr: false, bundle tersebut hanya dimuat ketika komponen di-render, meningkatkan Time to Interactive untuk pengguna yang tidak pernah berinteraksi dengan komponen tersebut.
Next.js 15 menyediakan beberapa lapisan caching: fetch cache (per-request, dengan revalidasi yang dapat dikonfigurasi), full-route cache (HTML yang di-render di-cache di layer CDN), dan data cache (deduplikasi request dalam satu render). Memahami cache mana yang berlaku untuk data Anda dan cara menginvalidasinya dengan benar sangat penting untuk membangun aplikasi yang cepat dan terkini.
Halaman yang sepenuhnya statis (generateStaticParams tanpa revalidasi) adalah yang tercepat — mereka disajikan dari node edge CDN tanpa pemrosesan server sama sekali. Incremental Static Regeneration (ISR) menambahkan interval revalidasi, memungkinkan halaman di-refresh di background sambil menyajikan respons cached yang cepat meskipun stale. Dynamic rendering berjalan di server per-request — gunakan hanya ketika konten harus sepenuhnya segar atau dipersonalisasi per pengguna.
Kesalahan performa Next.js yang paling umum adalah menambahkan 'use client' ke komponen induk, yang mengubah seluruh subtree-nya menjadi client component dan mengirimkan semua JavaScript tersebut ke browser. Jika hanya satu leaf component yang membutuhkan interaktivitas, hanya leaf tersebut yang harus menjadi Client Component. Teruskan data yang diambil server ke bawah sebagai props daripada mengambil ulang di client component.
Revalidasi berbasis waktu ISR berarti konten dapat kadaluarsa hingga N detik. Untuk konten yang harus segera diperbarui (blog post yang diterbitkan, perubahan harga), gunakan on-demand revalidation melalui revalidatePath() atau revalidateTag(). Panggil ini dari webhook handler yang dipicu oleh CMS atau database Anda, dan Next.js akan membersihkan dan meregenerasi hanya halaman yang terpengaruh.
Bundle JavaScript yang besar sering menjadi akar penyebab Time to Interactive yang buruk. Bundle analyzer bawaan Next.js (@next/bundle-analyzer) memberi Anda treemap visual bundle Anda, memudahkan untuk menemukan dependency yang tidak terduga besar. Script pihak ketiga — analitik, chat widget, A/B testing — sering menjadi pelanggar terbesar.
Aktifkan bundle analyzer dengan ANALYZE=true npm run build dan cari: paket duplikat (dua versi library yang sama), dependency besar yang bisa diganti dengan alternatif yang lebih ringan (lodash → lodash-es dengan tree shaking, atau native method), dan kode sisi client yang bisa dipindahkan ke Server Component. Targetkan First Load JS di bawah 100KB untuk jalur kritis.
Gunakan next/script dengan strategy='lazyOnload' untuk script pihak ketiga yang tidak kritis (analitik, chat). Ini menunda mereka sampai setelah halaman interaktif. Untuk script yang kritis tetapi tidak memblokir (Google Tag Manager), gunakan strategy='afterInteractive'. Strategi afterInteractive memuat script setelah hidrasi, mencegahnya memblokir LCP. Audit script pihak ketiga Anda secara teratur — setiap script menambah risiko pada Core Web Vitals Anda.
Gunakan direktif 'use server' dengan Server Action untuk menangani pengiriman form dan mutasi di sisi server tanpa overhead API route. Server Action mengurangi JavaScript sisi client dan menyediakan proteksi CSRF otomatis, deduplikasi request, dan keamanan tipe end-to-end TypeScript.
Siapkan real-user monitoring (RUM) dengan Vercel Analytics, Datadog RUM, atau library JavaScript web-vitals untuk menangkap Core Web Vitals dari pengguna nyata di produksi. Data lab dari Lighthouse memberi Anda baseline yang terkontrol; data RUM memberi tahu Anda apa yang sebenarnya dialami pengguna di perangkat dan koneksi mereka. Keduanya diperlukan. Ukur, optimalkan metrik dengan dampak tertinggi pertama, deploy, dan ukur lagi.
Konsep performa Next.js kunci dalam postingan ini meliputi LCP, CLS, INP, ISR, RSC, and code splitting.