D
P
0

Next.js

Halaman Artikel 200 di `pnpm dev` Tapi 500 di Produksi? `DYNAMIC_SERVER_USAGE` dari Fetch Server Component Bersarang

9 Juli 2026·4 menit baca
Halaman Artikel 200 di `pnpm dev` Tapi 500 di Produksi? `DYNAMIC_SERVER_USAGE` dari Fetch Server Component Bersarang

Sebuah halaman artikel di situs berbasis Next.js yang saya kerjakan untuk klien tampil sempurna di lokal. Saya jalankan pnpm dev, buka route-nya, dan dapat 200 dengan render lengkap: judul, isi, dan sebuah chart harga koin di tengah artikel. Semua hijau. Saya push, Vercel build sukses, lalu saya buka URL produksi yang sama dan dapat halaman putih dengan teks polos: Internal Server Error. HTTP 500.

Yang bikin saya bingung: bukan semua artikel yang 500. Route saudaranya, artikel yang tidak menyisipkan chart, tetap 200 dan render normal. Hanya artikel yang memicu chart yang tumbang. Dan ini yang paling menyesatkan: masalahnya bertahan melewati setiap purge cache yang saya coba. Saya redeploy dengan cache kosong, saya invalidasi CDN, hasilnya tetap sama. Ketika sebuah bug selamat dari purge cache, itu bukan masalah cache. Itu deterministik.

Menelusuri jejaknya

Karena dev bersih dan hanya produksi yang gagal, saya tahu ini tipe bug "beda perilaku antara dev dan build". Dev mode Next.js itu permisif; dia me-render on demand dan jarang mengeksekusi jalur optimasi statis yang dipakai next build. Jadi hal pertama yang saya lakukan bukan menebak, tapi mereproduksi kondisi produksi di lokal:

pnpm build && pnpm start

Dan benar saja, di sinilah 500 itu muncul di mesin saya sendiri. Sekarang saya bisa membaca stderr yang sebenarnya alih-alih menebak dari halaman error klien. Di log, digest error-nya jelas: DYNAMIC_SERVER_USAGE. Itu petunjuk yang saya butuhkan. Sesuatu di route itu menyentuh API dinamis pada saat Next.js justru berusaha meng-optimasi route-nya secara statis.

Saya lacak rantai pemanggilannya. Halaman artikel me-render komponen chart bersarang, dan chart itu punya rantai fetch sendiri:

ArticleCoinChart -> fetchCoinChart -> getApiSettings -> sanityFetch -> draftMode()

Di ujung rantai itu ada draftMode(). Itu API dinamis: memanggilnya menandai render sebagai bergantung pada request. Selama ini sanityFetch memang membaca draftMode() untuk memutuskan menyajikan draft atau konten publish. Tidak ada yang salah dengan kode itu secara isolasi. Yang salah adalah konteks tempat ia dipanggil.

Kenapa produksi 500 sementara dev 200

Route artikel ini mendeklarasikan generateStaticParams() yang mengembalikan array kosong:

export async function generateStaticParams() {
  return [];
}

Niat saya waktu itu: "jangan pre-render apa pun saat build, render semuanya on demand." Tapi mengembalikan [] tidak sama dengan mematikan optimasi statis. Next.js 16 tetap memperlakukan route ini sebagai kandidat untuk dioptimasi statis, dan mencoba merender-nya dalam konteks tanpa request. Di dalam konteks itulah draftMode() yang bersarang jauh di dalam komponen chart tersentuh, dan itu memicu DYNAMIC_SERVER_USAGE saat request nyata masuk.

Dev tidak pernah memunculkan ini karena dev tidak menjalankan optimasi statis itu. Setiap request di dev dirender penuh dalam konteks dinamis, jadi draftMode() selalu punya request untuk disandarkan. Itu sebabnya lokal hijau dan produksi merah untuk kode yang persis sama.

Perbaikannya

Ada dua jalan bersih, dan keduanya sama-sama benar tergantung selera arsitektur.

Yang pertama: angkat fetch ke level page. Halaman artikel sudah hidup di konteks dinamis, jadi kalau draftMode() tersentuh di sana, tidak ada masalah. Saya ambil hasil chart di page, lalu oper ke bawah sebagai prop ke ArticleCoinChart, bukan membiarkan komponen bersarang itu men-fetch sendiri.

Yang kedua, dan yang akhirnya saya pakai karena paling minim perubahan: paksa route-nya dinamis secara eksplisit.

export const dynamic = "force-dynamic";

Satu baris itu memberi tahu Next.js untuk tidak pernah mencoba meng-optimasi route ini secara statis. Tidak ada lagi render tanpa request, jadi draftMode() yang bersarang selalu punya konteks yang sah. 500 hilang.

Satu jebakan yang perlu dicatat: export const revalidate = N saja TIDAK cukup di kasus ini. Ketika generateStaticParams() mengembalikan [], revalidate tidak mencegah percobaan optimasi statis yang memicu masalah. Kalau ada API dinamis di jalur render, kamu butuh force-dynamic, bukan sekadar revalidasi.

Pelajaran

Bug ini punya dua pelajaran yang saling menguatkan. Pertama, jangan pernah percaya pnpm dev untuk memvalidasi perilaku produksi pada apa pun yang menyangkut rendering statis versus dinamis. Dev mode terlalu permisif; ia menyembunyikan seluruh kelas bug yang hanya muncul di next build. Sekarang, untuk setiap fetch yang saya tambahkan ke komponen server bersarang, saya jalankan pnpm build && pnpm start sebelum push, bukan sesudah.

Kedua, ketika produksi tampak "macet" atau menyajikan error yang bertahan melewati purge cache, curigai crash render deterministik, bukan cache. Membaca stderr produksi asli, DYNAMIC_SERVER_USAGE dalam kasus ini, memangkas berjam-jam tebakan. API dinamis yang tersembunyi jauh di dalam rantai komponen bersarang tetap menghitung; ia tidak peduli seberapa dalam ia dikubur. Kalau sebuah route yang mengembalikan generateStaticParams kosong menyentuh draftMode(), cookies(), atau headers() di mana pun di pohonnya, deklarasikan force-dynamic dan berhenti melawan optimizer.