Cara Meningkatkan Performa Website dengan React dan Next.js
Panduan praktis membuat website React dan Next.js lebih cepat: ukur Core Web Vitals, kirim lebih sedikit JavaScript dengan Server Components, pre-render dengan SSG, dan optimalkan gambar, font, serta script pihak ketiga.
Performa adalah fitur. Halaman yang lambat kehilangan pengunjung sebelum mereka sempat melihat karyamu, dan sejak Core Web Vitals menjadi sinyal peringkat Google, halaman lambat juga mengurangi trafik dari mesin pencari. Kabar baiknya, React dan Next.js sudah menyediakan hampir semua alat yang kamu butuhkan. Kamu hanya perlu memakainya dengan sengaja.
Belum lama ini saya membangun ulang portfolio ini dari aplikasi Vite yang di-render di sisi klien menjadi Next.js. Berikut teknik-teknik yang paling berpengaruh, lengkap dengan kode yang bisa langsung kamu pakai di proyekmu sendiri.
1. Ukur dulu sebelum mengoptimalkan
Menebak-nebak adalah cara tercepat untuk membuang waktu seharian. Mulailah dari tiga metrik Core Web Vitals yang dipakai Google untuk menilai pengalaman pengguna sungguhan:
- LCP (Largest Contentful Paint): seberapa cepat konten utama muncul. Targetkan 2,5 detik atau kurang.
- INP (Interaction to Next Paint): seberapa cepat halaman merespons klik dan ketikan. Targetkan 200 ms atau kurang.
- CLS (Cumulative Layout Shift): seberapa banyak tata letak bergeser saat halaman dimuat. Targetkan 0,1 atau kurang.
Gunakan Lighthouse atau PageSpeed Insights untuk data lab, lalu pastikan dengan data lapangan (field data) dari pengguna sungguhan. Next.js menyediakan metrik ini lewat useReportWebVitals, jadi kamu bisa mengirimkannya ke tool analytics apa pun:
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
// Replace with your analytics endpoint
console.log(metric.name, Math.round(metric.value));
});
return null;
}2. Render di server dan kirim lebih sedikit JavaScript
Di App Router, setiap komponen secara default adalah React Server Component. Kodenya dijalankan di server dan tidak pernah dikirim ke browser, sehingga tidak menambah satu byte pun ke bundle JavaScript. Hanya komponen yang ditandai "use client" yang dikirim ke browser lalu di-hydrate.
Aturan praktisnya: letakkan "use client" sedalam mungkin di pohon komponen. Biarkan page dan layout tetap di server, dan jadikan Client Component hanya bagian-bagian kecil yang interaktif.
// Server Component: none of this code is sent to the browser
import { LikeButton } from "./like-button"; // the only client code on this page
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<PostBody content={post.content} />
<LikeButton slug={slug} />
</article>
);
}JavaScript tercepat adalah JavaScript yang tidak pernah kamu kirim ke browser.
3. Pre-render halaman dengan SSG dan ISR
Kalau sebuah halaman terlihat sama untuk semua pengunjung, tidak ada alasan untuk me-render-nya di setiap request. Static Site Generation (SSG) membuat HTML sekali saat build, lalu menyajikannya langsung dari CDN. Ini hampir secepat yang bisa dicapai di web, dan artikel ini pun di-render dengan cara itu.
// Build one static HTML page per post at build time
export const generateStaticParams = () =>
blogs.map((post) => ({ slug: post.slug }));
// Any slug not returned above becomes a 404 instead of a server render
export const dynamicParams = false;Kalau kontennya berubah secara berkala, gunakan Incremental Static Regeneration (ISR). Halaman tetap statis, dan Next.js membangunnya ulang di latar belakang paling sering sekali per interval:
// Regenerate this page at most once an hour
export const revalidate = 3600;Simpan dynamic rendering untuk halaman yang benar-benar bergantung pada request, seperti dashboard, keranjang belanja, atau apa pun yang membaca cookie.
4. Optimalkan gambar dengan next/image
Gambar biasanya bagian terberat dari sebuah halaman dan paling sering menjadi elemen LCP. Di situs ini, logo berukuran 40 px di navbar dulunya dikirim sebagai JPEG berukuran 2481 px dan 167 KB. Beralih ke next/image menyelesaikannya hanya dengan satu baris.
- Gambar diubah ukurannya sesuai ukuran tampilannya dan dikirim dalam format modern seperti WebP dan AVIF.
- Gambar di bawah layar pertama otomatis di-lazy-load.
- Ruang untuk gambar sudah disiapkan berdasarkan lebar dan tingginya, sehingga tidak terjadi layout shift (CLS).
import Image from "next/image";
import hero from "@/assets/images/hero.webp";
export function Hero() {
return (
<Image
src={hero}
alt="Dashboard preview"
sizes="(min-width: 1024px) 50vw, 100vw"
placeholder="blur"
preload // the LCP image: load it early (called "priority" before Next.js 16)
/>
);
}Selalu isi atribut sizes untuk gambar responsif. Tanpanya, browser bisa mengunduh file yang jauh lebih besar dari yang dibutuhkan tata letak.
5. Muat font dengan next/font
Memuat Google Fonts lewat @import di CSS akan menahan proses render: browser harus mengambil stylesheet-nya dulu, lalu file font-nya, baru bisa menampilkan teks. next/font mengunduh font saat build, menyajikannya dari domain kamu sendiri, dan membuat fallback dengan ukuran yang disesuaikan supaya teks tidak melompat saat font aslinya selesai dimuat.
import { Syne } from "next/font/google";
const syne = Syne({
subsets: ["latin"],
display: "swap",
variable: "--font-syne",
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={syne.variable}>
<body>{children}</body>
</html>
);
}6. Pecah komponen berat dengan next/dynamic
Chart, peta, rich text editor, dan video player masing-masing bisa menambah ratusan kilobyte. Kalau komponen itu tidak dibutuhkan untuk tampilan pertama, muat hanya saat komponen tersebut di-render:
"use client";
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("./revenue-chart"), {
ssr: false,
loading: () => <div className="h-80 animate-pulse rounded-2xl bg-secondary" />,
});Buat placeholder loading setinggi komponen aslinya. Kalau tidak, kamu hanya menukar bundle yang lebih kecil dengan layout shift.
7. Jaga interaksi tetap responsif
Muatan awal yang cepat tidak banyak artinya kalau halaman macet saat seseorang mengetik. Itulah yang diukur oleh INP. React menyediakan dua alat untuk mendahulukan update yang mendesak (mengetik, mengklik) di atas update yang berat (memfilter daftar panjang):
useDeferredValuemembuat input langsung ter-update, sementara hasil yang berat menyusul belakangan.useTransitionmenandai sebuah state update sebagai tidak mendesak, sehingga React bisa menundanya demi input pengguna.
"use client";
import { useDeferredValue, useState } from "react";
export function ProjectSearch({ projects }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = projects.filter((project) =>
project.title.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ProjectList items={results} />
</>
);
}Penundaan ini hanya membantu kalau ProjectList tidak re-render ketika props-nya tidak berubah. Daripada menaburkan memo dan useMemo di mana-mana, aktifkan React Compiler yang menambahkan memoization itu secara otomatis. Di Next.js 16, install babel-plugin-react-compiler lalu set reactCompiler: true di next.config.ts.
8. Kendalikan script pihak ketiga
Widget chat, tag manager, dan tracking pixel sering menjadi biang lambatnya halaman karena berjalan di main thread saat halaman sedang dimuat. Muat semuanya dengan next/script dan pilih strategy yang sesuai dengan seberapa penting script tersebut:
import Script from "next/script";
// Not needed for the first paint: load it once the browser is idle
<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />Sebelum menambahkan script apa pun, tanyakan dulu apakah kamu benar-benar membutuhkannya. Script pihak ketiga yang paling murah adalah script yang kamu hapus.
9. Ambil data secara paralel dan simpan di cache
Pemanggilan await secara berurutan menciptakan waterfall: setiap request menunggu request sebelumnya selesai. Kalau request-request itu tidak saling bergantung, jalankan bersamaan:
// Slow: the second request waits for the first
const profile = await getProfile();
const projects = await getProjects();
// Fast: both requests run at the same time
const [profile, projects] = await Promise.all([getProfile(), getProjects()]);Lalu simpan di cache data yang tidak berubah di setiap request. Dengan fetch di Server Component, satu opsi saja sudah cukup untuk menyimpan response di cache dan memperbaruinya di latar belakang:
const res = await fetch("https://api.example.com/projects", {
next: { revalidate: 3600 }, // reuse the response for up to an hour
});Checklist singkat
- Ukur LCP, INP, dan CLS dengan data pengguna sungguhan sebelum mengubah apa pun.
- Biarkan komponen tetap di server dan tambahkan
"use client"hanya di bagian yang butuh interaktivitas. - Pre-render halaman statis dengan
generateStaticParams, dan gunakanrevalidateuntuk konten yang berubah. - Sajikan semua gambar lewat
next/imagedengansizesyang benar, dan preload gambar LCP. - Muat font dengan
next/font, bukan lewat import CSS. - Lazy-load komponen berat yang berada di bawah layar pertama dengan
next/dynamic. - Gunakan
useDeferredValue,useTransition, dan React Compiler agar interaksi tetap cepat. - Muat script pihak ketiga dengan
next/script, atau hapus saja. - Jalankan request data yang tidak saling bergantung secara paralel, lalu simpan hasilnya di cache.
Kamu tidak perlu melakukan semuanya sekaligus. Pilih metrik yang paling buruk di situsmu, perbaiki penyebab terbesarnya, ukur lagi, lalu ulangi. Perbaikan kecil yang terukur akan terkumpul menjadi situs yang terasa instan.