Sebuah situs klien berbasis Next.js 16 punya halaman detail koin di route /coins/[slug]. Tiap koin dapat halamannya sendiri: harga, statistik, dan sebuah chart yang bisa diganti rentang waktunya lewat tombol 24 jam, 7 hari, 30 hari. Di lokal semua mulus. Saya jalankan pnpm dev, buka /coins/bitcoin, dapat 200 dengan render penuh. Saya push ke Railway, build sukses, buka URL produksi yang persis sama, dan dapat Internal Server Error. HTTP 500. Bukan sekali, tapi konsisten, dan selalu setelah loading sekitar lima detik.
Yang bikin ini menonjol: bukan cuma satu koin. Semua route /coins/[slug] 500 di produksi. Sementara itu route lain, homepage, halaman list, halaman about, semuanya 200 dan sehat. Jadi ada sesuatu yang spesifik di template detail koin ini yang tumbang, dan hanya di build produksi.
Jalan buntu pertama
Tebakan pertama saya salah, dan saya buang waktu di situ. Karena halaman detail baca banyak field dari API, saya curiga ada null-deref. Salah satu field yang mencurigakan adalah athDate, tanggal all-time-high yang kadang belum tersedia untuk koin baru. Saya pikir render meledak karena akses properti pada nilai null, jadi saya tambal dengan optional chaining di mana-mana.
const ath = coin.athDate?.toLocaleDateString();Push lagi. Masih 500. Tentu saja, karena null-deref itu tidak pernah benar-benar terjadi. Kalau memang ada TypeError, dev juga akan meledak, dan dev bersih. Saya menembak gejala yang salah karena saya belum membaca log yang sebenarnya. Pelajaran klasik: jangan menambal sebelum tahu error-nya.
Membaca error yang sebenarnya
Karena dev hijau dan hanya produksi merah, ini jelas tipe bug beda perilaku antara dev dan build. Dev mode Next.js permisif; ia me-render on demand dan tidak menjalankan jalur optimasi statis yang dipakai next build. Jadi saya berhenti menebak dan mereproduksi kondisi produksi di mesin sendiri:
pnpm build && pnpm start500 itu langsung muncul di lokal. Sekarang saya bisa baca stderr asli, bukan menebak dari halaman error klien. Digest-nya gamblang:
Error: digest 'DYNAMIC_SERVER_USAGE'Itu petunjuk yang saya butuhkan. Ada API dinamis yang tersentuh justru saat Next.js mencoba meng-optimasi route ini secara statis.
Akar masalah: searchParams plus generateStaticParams kosong
Route detail koin punya dua hal yang, sendiri-sendiri, tampak tidak berbahaya.
Pertama, ia mendeklarasikan generateStaticParams() yang mengembalikan array kosong:
export async function generateStaticParams() {
return [];
}Niat saya: jangan pre-render koin apa pun saat build, render semuanya on demand. Tapi mengembalikan array kosong tidak sama dengan mematikan optimasi statis. Next.js 16 tetap memperlakukan route ini sebagai kandidat statis.
Kedua, page-nya membaca searchParams untuk tahu rentang chart yang aktif:
export default async function CoinPage({ searchParams }) {
const range = searchParams.range ?? "24h";
const chart = await getCoinChart(slug, range);
// ...
}searchParams adalah API dinamis. Membacanya menandai render sebagai bergantung pada request. Di sinilah konfliknya: Next.js mencoba meng-optimasi route secara statis karena generateStaticParams kosong, tapi render-nya menyentuh searchParams yang menuntut konteks request. Statis versus dinamis bertabrakan, dan tabrakan itu meledak jadi DYNAMIC_SERVER_USAGE di request time, cuma di build produksi.
Penting dicatat: ini beda dari kasus yang pernah saya tulis di mana API dinamisnya adalah draftMode() yang terkubur dalam rantai fetch komponen bersarang. Di sini tidak ada yang bersarang. searchParams dibaca langsung di baris paling atas page. Justru karena kelihatan biasa, ia gampang terlewat.
Dev tidak pernah memunculkan ini karena dev selalu me-render dalam konteks dinamis, jadi searchParams selalu punya request untuk disandarkan. Itu sebabnya lokal hijau dan produksi merah untuk kode yang persis sama.
Perbaikannya
Satu baris. Buang generateStaticParams kosong itu, ganti dengan deklarasi dinamis eksplisit:
export const dynamic = "force-dynamic";Ini memberi tahu Next.js untuk tidak pernah mencoba meng-optimasi route ini secara statis. Tidak ada lagi render tanpa request, jadi searchParams selalu punya konteks yang sah. 500 hilang, dan tombol rentang chart bekerja seperti seharusnya.
Pelajaran
generateStaticParamsyang mengembalikan array kosong bukan cara mematikan optimasi statis. Next.js 16 tetap menganggap route-nya kandidat statis.- Empty
generateStaticParamsaman HANYA kalau render cuma menyentuhparams. Begitu ia bacasearchParams,cookies(),headers(), ataudraftMode(), kamu butuhforce-dynamic. - Jangan menambal sebelum baca error. Optional chaining di
athDateitu jalan buntu karena null-deref-nya memang tidak pernah terjadi. - Reproduksi produksi dengan
pnpm build && pnpm start.pnpm devmenyembunyikan seluruh kelas bug statis versus dinamis. - Kalau produksi 500 konsisten dan bertahan, baca stderr asli. Digest
DYNAMIC_SERVER_USAGEmemangkas berjam-jam tebakan.
