Pada aplikasi Next.js modern, ukuran client bundle sangat berpengaruh pada performa nyata di browser. Semakin banyak JavaScript yang harus diunduh, di-parse, dan dieksekusi, semakin lama halaman menjadi benar-benar interaktif. Salah satu sumber pemborosan yang paling umum adalah menaruh terlalu banyak logika di komponen client, padahal banyak bagian UI sebenarnya bisa dirender di server.
Di Next.js, pendekatan yang umumnya paling efektif adalah memaksimalkan Server Component dan hanya memberi directive use client pada komponen yang memang membutuhkan interaktivitas browser, seperti state lokal, event handler, akses DOM, atau hook client-side. Strategi ini bukan sekadar soal gaya arsitektur, tetapi berdampak langsung pada ukuran bundle, waktu hydration, dan Time to Interactive (TTI).
Artikel ini membahas cara kerja pendekatan tersebut, contoh refactor halaman e-commerce dari seluruhnya client menjadi hybrid, penggunaan dynamic import, cara membaca analisis bundle, serta teknik mengidentifikasi dependensi besar yang seharusnya tidak ikut terkirim ke browser.
Mengapa ukuran bundle client penting
Bundle client bukan hanya biaya unduh. Browser harus melakukan beberapa tahap sebelum pengguna bisa berinteraksi dengan nyaman:
- Mengunduh aset JavaScript.
- Melakukan parse dan compile.
- Menjalankan modul.
- Melakukan hydration pada komponen interaktif.
Pada perangkat cepat, dampaknya mungkin terasa kecil. Namun di perangkat menengah ke bawah atau jaringan lambat, bundle yang besar bisa memperlambat rendering interaktif secara signifikan. TTI yang buruk sering bukan disebabkan HTML awal lambat tampil, tetapi karena JavaScript terlalu banyak.
Di sinilah pemisahan komponen menjadi penting. Jika data fetching, formatting, rendering daftar produk, dan layout statis bisa dilakukan di server, maka browser tidak perlu memuat seluruh logika itu. Browser cukup menerima HTML hasil render dan JavaScript minimal untuk bagian yang benar-benar interaktif.
Prinsip dasar: default ke Server Component, naikkan ke client hanya jika perlu
Kesalahan umum di proyek Next.js adalah memberi use client terlalu tinggi dalam pohon komponen. Saat sebuah file diberi directive tersebut, komponen itu menjadi Client Component, dan seluruh subtree yang diimpor langsung dari dalamnya cenderung ikut masuk ke dunia client, kecuali dipisahkan secara tepat. Akibatnya, modul yang sebenarnya aman dirender di server bisa terseret ke bundle browser.
Sebagai aturan praktis:
- Gunakan Server Component untuk data fetching, formatting data, komposisi layout, render konten statis, dan akses resource server.
- Gunakan Client Component hanya untuk state lokal, event klik, form interaktif, akses window/document, animasi client-side, atau hook seperti useState dan useEffect.
- Letakkan boundary client sedekat mungkin ke elemen interaktif.
Jika sebuah tombol butuh interaksi, sering kali yang perlu menjadi client hanyalah tombol itu atau widget kecil di sekitarnya, bukan seluruh halaman.
Contoh masalah: halaman e-commerce yang seluruhnya client
Misalkan ada halaman detail produk yang awalnya dibuat sepenuhnya sebagai Client Component. Pendekatan ini memang terasa mudah, karena semua logika berada di satu tempat. Namun dampaknya adalah browser harus memuat kode untuk data fetching, formatting harga, galeri, review, dan widget lain meskipun tidak semuanya memerlukan interaktivitas.
Versi awal: semua di client
'use client'
import { useEffect, useState } from 'react'
import Image from 'next/image'
import { Carousel } from 'some-heavy-carousel-lib'
import { formatCurrency } from '@/lib/format'
import { AddToCartButton } from '@/components/add-to-cart-button'
import { ProductReviews } from '@/components/product-reviews'
export default function ProductPage({ params }) {
const [product, setProduct] = useState(null)
useEffect(() => {
fetch(`/api/products/${params.slug}`)
.then((res) => res.json())
.then(setProduct)
}, [params.slug])
if (!product) return <div>Loading...</div>
return (
<div>
<h1>{product.name}</h1>
<Carousel>
{product.images.map((src) => (
<Image key={src} src={src} alt={product.name} width={800} height={800} />
))}
</Carousel>
<p>{formatCurrency(product.price)}</p>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
<ProductReviews productId={product.id} />
</div>
)
}Ada beberapa masalah di sini:
- Data produk diambil dari browser, sehingga konten utama terlambat tersedia.
- Seluruh halaman menjadi client, termasuk bagian yang sebenarnya statis.
- Library carousel yang berat ikut masuk ke bundle awal.
- Komponen review mungkin membawa dependency tambahan ke client, padahal bisa ditunda.
Refactor ke arsitektur hybrid
Tujuan refactor adalah menjadikan halaman produk sebagai Server Component, lalu mengekstrak interaktivitas ke komponen client kecil. Dengan pendekatan ini, konten utama bisa dirender di server, sementara browser hanya menerima JavaScript untuk fitur interaktif yang diperlukan.
Langkah 1: pindahkan halaman utama ke server
import { getProductBySlug } from '@/lib/data'
import { formatCurrency } from '@/lib/format'
import AddToCartButton from './add-to-cart-button'
import ProductGallery from './product-gallery'
import ReviewsSection from './reviews-section'
export default async function ProductPage({ params }) {
const product = await getProductBySlug(params.slug)
return (
<div>
<h2>{product.name}</h2>
<ProductGallery images={product.images} productName={product.name} />
<p>{formatCurrency(product.price)}</p>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
<ReviewsSection productId={product.id} />
</div>
)
}Di tahap ini, halaman tidak memakai use client. Data diambil di server, sehingga HTML awal sudah berisi nama produk, harga, dan deskripsi. Ini mengurangi kebutuhan JavaScript di sisi browser untuk menampilkan konten utama.
Langkah 2: buat komponen client sekecil mungkin
'use client'
import { useState } from 'react'
export default function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false)
async function handleClick() {
setLoading(true)
try {
await fetch('/api/cart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, quantity: 1 })
})
} finally {
setLoading(false)
}
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Menambahkan...' : 'Tambah ke Keranjang'}
</button>
)
}Komponen ini memang perlu menjadi client karena memiliki event handler dan state lokal. Namun ukurannya kecil dan fokus.
Langkah 3: tunda bagian berat dengan dynamic import
Galeri gambar interaktif atau review yang kompleks sering kali tidak perlu masuk ke bundle awal. Jika bagian tersebut tidak kritikal untuk interaksi pertama, dynamic import dapat menunda pemuatan sampai dibutuhkan.
import dynamic from 'next/dynamic'
const ProductGallery = dynamic(() => import('./product-gallery-client'), {
loading: () => <div>Memuat galeri...</div>
})
const ReviewsSection = dynamic(() => import('./reviews-section-client'), {
loading: () => <div>Memuat ulasan...</div>
})
export default function ProductEnhancements({ images, productId, productName }) {
return (
<>
<ProductGallery images={images} productName={productName} />
<ReviewsSection productId={productId} />
</>
)
}Dynamic import membantu memecah bundle berdasarkan batas komponen. Hasilnya, JavaScript untuk carousel atau sistem review tidak perlu dimuat bersama bundle awal halaman. Ini sangat berguna jika dependency internalnya besar.
Namun perlu dipahami trade-off-nya: jika komponen yang di-dynamic import sebenarnya langsung terlihat dan harus segera interaktif, pengguna bisa melihat fase loading tambahan. Jadi, gunakan teknik ini untuk fitur sekunder, konten di bawah fold, modal, editor, chart, peta, atau widget yang tidak selalu dipakai.
Mengidentifikasi dependensi besar yang tidak perlu ke browser
Optimasi bundle sering gagal karena masalah utamanya bukan pada kode aplikasi, melainkan dependency pihak ketiga. Satu package besar yang diimpor dari Client Component bisa membengkakkan bundle secara signifikan.
Tanda-tanda dependency bermasalah
- Library hanya dipakai untuk formatting atau transformasi data, tetapi diimpor di Client Component.
- Package UI membawa banyak fitur, padahal yang dipakai hanya satu widget kecil.
- Library yang idealnya berjalan di server, seperti parser, SDK admin, atau utilitas berat, ikut terimpor ke client.
- Import dari entry point besar, misalnya mengimpor seluruh package daripada submodule yang dibutuhkan.
Cara praktis menemukannya
- Lihat boundary use client. Periksa file yang diberi directive tersebut. Audit semua import di dalamnya.
- Gunakan bundle analyzer. Visualisasi ukuran chunk membantu melihat modul mana yang dominan.
- Cek transitive dependencies. Kadang package kecil mengimpor package lain yang sangat besar.
- Pindahkan utilitas ke server bila memungkinkan. Formatting harga, normalisasi data, perhitungan, atau agregasi sering tidak perlu berjalan di browser.
Contoh pola yang perlu dihindari
'use client'
import _ from 'lodash'
import dayjs from 'dayjs'
import { expensiveProductTransform } from '@/lib/product-transform'
export function ProductCard({ product }) {
const normalized = expensiveProductTransform(product)
return <div>{dayjs(product.createdAt).format('DD/MM/YYYY')}</div>
}Jika transformasi data dapat dilakukan di server sebelum dikirim ke komponen, lakukan di sana. Bahkan untuk formatting tanggal atau harga, sering lebih efisien mengirim hasil akhir yang siap render daripada membawa helper tambahan ke browser, terutama jika helper tersebut menarik dependency lain.
Refactor yang lebih baik:
import { getProductCardViewModel } from '@/lib/product-view-model'
import ProductCardClient from './product-card-client'
export default async function ProductCardContainer({ productId }) {
const viewModel = await getProductCardViewModel(productId)
return <ProductCardClient product={viewModel} />
}Dengan pola ini, normalisasi dan formatting dilakukan di server, sedangkan komponen client hanya menerima data final yang dibutuhkan untuk interaksi.
Analisis bundle dan cara membacanya
Setelah refactor, jangan berhenti pada asumsi. Ukur hasilnya. Salah satu langkah penting adalah menjalankan analisis bundle untuk melihat modul apa saja yang masuk ke client chunk.
Konfigurasi umum dengan analyzer biasanya mirip berikut:
import withBundleAnalyzer from '@next/bundle-analyzer'
const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true'
})
export default bundleAnalyzer({
reactStrictMode: true
})Kemudian jalankan build dengan analyzer aktif:
ANALYZE=true next buildSaat laporan terbuka, fokus pada beberapa hal:
- Chunk halaman: apakah halaman produk membawa library besar yang tidak relevan untuk render awal?
- Shared chunks: apakah ada package besar yang terbagi ke banyak halaman karena diimpor di komponen client bersama?
- Duplikasi dependency: apakah dua package berbeda membawa fungsi serupa?
Jangan hanya melihat total ukuran. Perhatikan juga apa yang memaksa modul tertentu masuk ke client. Sering kali akar masalahnya adalah satu import di file dengan use client.
Dampak pada TTI dan hydration
Ketika lebih banyak UI dipindahkan ke Server Component, ada beberapa efek positif yang umumnya terjadi:
- Bundle JavaScript awal berkurang.
- Browser melakukan parse dan execute lebih sedikit kode.
- Hydration hanya terjadi pada pulau interaktif kecil, bukan seluruh halaman.
- Konten utama lebih cepat siap tampil karena berasal dari HTML server-rendered.
Semua ini cenderung membantu TTI, terutama pada halaman konten-heavy seperti e-commerce, katalog, blog, atau dashboard yang memiliki campuran antara konten statis dan widget interaktif.
Namun perlu dicatat, TTI tidak hanya dipengaruhi bundle size. Jika Anda menambah terlalu banyak boundary terpisah atau terlalu agresif melakukan lazy loading, pengguna bisa mengalami banyak loading state kecil. Jadi optimasi harus tetap mempertimbangkan pengalaman pengguna secara keseluruhan.
Trade-off developer experience
Pemisahan Server dan Client Component membuat aplikasi lebih efisien, tetapi ada biaya mental bagi developer.
Trade-off yang umum
- Boundary lebih eksplisit. Developer harus sadar kapan kode berjalan di server dan kapan di browser.
- Aturan import lebih ketat. Tidak semua module aman dipakai dari Client Component.
- Debugging sedikit berbeda. Sebagian bug muncul saat serialisasi props atau saat modul browser-only tidak sengaja dipakai di server.
- Refactor butuh disiplin. Menjaga agar use client tidak naik ke level terlalu atas memerlukan review kode yang konsisten.
Meski demikian, trade-off ini biasanya sepadan untuk aplikasi yang sensitif terhadap performa. Kuncinya adalah membuat pola kerja yang jelas: komponen server sebagai default, client untuk island interaktif, dan audit dependency sebagai bagian rutin dari code review.
Kesalahan yang sering terjadi
- Menaruh use client di file page atau layout tanpa kebutuhan nyata.
- Mengimpor library berat di komponen client untuk tugas yang bisa dilakukan di server.
- Menggunakan dynamic import untuk hampir semua hal sehingga UI dipenuhi loading placeholder.
- Tidak mengukur hasil optimasi dengan analyzer atau profiling.
- Menganggap semua rendering server otomatis lebih cepat, padahal interaksi tertentu memang lebih cocok ditangani penuh di client.
Checklist praktis untuk optimasi bundle client
- Audit semua file yang memiliki use client.
- Turunkan boundary client ke komponen paling kecil yang benar-benar interaktif.
- Pindahkan data fetching, formatting, dan transformasi ke Server Component bila memungkinkan.
- Gunakan dynamic import untuk widget berat yang tidak kritikal pada render awal.
- Analisis bundle setelah build dan identifikasi modul terbesar di client chunks.
- Tinjau dependency pihak ketiga yang terbawa ke browser dari komponen client.
- Ukur hasilnya pada perangkat dan jaringan yang realistis, bukan hanya localhost.
Penutup
Optimasi bundle client di Next.js bukan soal mengejar angka sekecil mungkin, melainkan memastikan browser hanya menerima JavaScript yang benar-benar dibutuhkan untuk interaksi. Strategi paling efektif biasanya sederhana: render sebanyak mungkin di server, kirim sesedikit mungkin ke client.
Pada halaman e-commerce, ini berarti konten produk, harga, deskripsi, dan struktur utama dirender sebagai Server Component, sementara tombol keranjang, galeri interaktif, dan review dinyalakan sebagai pulau client yang terisolasi. Tambahkan dynamic import untuk fitur berat yang tidak kritikal, lalu validasi keputusan Anda dengan bundle analyzer.
Jika dilakukan dengan disiplin, pendekatan ini dapat membantu memperbaiki TTI, mengurangi biaya hydration, dan menjaga pengalaman pengguna tetap responsif tanpa mengorbankan fitur interaktif yang memang dibutuhkan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!