D
P
0

Next.js

Route Detail 500 Cuma di Produksi, `pnpm dev` Aman? `DYNAMIC_SERVER_USAGE` dari `generateStaticParams` Kosong yang Baca `searchParams`

31 Juli 2026·4 menit baca
Route Detail 500 Cuma di Produksi, `pnpm dev` Aman? `DYNAMIC_SERVER_USAGE` dari `generateStaticParams` Kosong yang Baca `searchParams`

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 start

500 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

  • generateStaticParams yang mengembalikan array kosong bukan cara mematikan optimasi statis. Next.js 16 tetap menganggap route-nya kandidat statis.
  • Empty generateStaticParams aman HANYA kalau render cuma menyentuh params. Begitu ia baca searchParams, cookies(), headers(), atau draftMode(), kamu butuh force-dynamic.
  • Jangan menambal sebelum baca error. Optional chaining di athDate itu jalan buntu karena null-deref-nya memang tidak pernah terjadi.
  • Reproduksi produksi dengan pnpm build && pnpm start. pnpm dev menyembunyikan seluruh kelas bug statis versus dinamis.
  • Kalau produksi 500 konsisten dan bertahan, baca stderr asli. Digest DYNAMIC_SERVER_USAGE memangkas berjam-jam tebakan.