Bug ini bikin saya bingung hampir satu sore. Di sebuah situs Next.js yang saya bangun, tag analytics tidak pernah muncul di production — padahal NEXT_PUBLIC_GA_ID jelas-jelas sudah saya isi di environment variable dashboard hosting, dan nilainya benar. Di lokal jalan. Di production: undefined.
Yang bikin makin aneh: saya cek environment runtime di server, variabelnya ada. Tapi di bundle yang dikirim ke browser, nilainya kosong.
Penyebabnya adalah hal yang sering disalahpahami soal Next.js: NEXT_PUBLIC_* itu bukan dibaca saat runtime — ia di-substitusi saat build.
NEXT_PUBLIC_* adalah penggantian string saat compile
Saat Anda menjalankan next build, Next.js mencari setiap kemunculan process.env.NEXT_PUBLIC_XXX di kode dan mengganti teksnya langsung dengan nilai literal yang ada saat build berjalan. Hasilnya di-hardcode ke dalam bundle JavaScript.
Artinya: nilai yang "menempel" adalah nilai yang tersedia pada saat next build dieksekusi — bukan saat aplikasi berjalan. Kalau Anda mengisi variabel di dashboard setelah build (atau build memakai cache lama tanpa variabel itu), bundle Anda sudah terlanjur berisi undefined. Mengubah runtime env setelahnya tidak mengubah apa pun, karena tidak ada lagi process.env yang dibaca di browser — yang ada hanya string yang sudah dibaked.
Inilah kenapa "variabelnya ada di server tapi undefined di browser" terjadi: server runtime dan build-time itu dua momen berbeda.
Perbaikannya: build ulang yang benar-benar fresh
Solusinya bukan menyentuh kode, tapi memastikan variabel tersedia saat build:
- Pastikan
NEXT_PUBLIC_*sudah terisi di environment sebelumnext builddijalankan (di scope build, bukan cuma runtime). - Picu build baru yang benar-benar fresh — bukan "redeploy" yang memakai cache build sebelumnya. Banyak platform punya tombol "Redeploy" yang reuse artefak lama; itu tidak akan menanamkan variabel baru. Cari opsi "Clear build cache" atau commit kosong untuk memaksa build penuh.
# Verifikasi nilainya benar-benar masuk ke bundle (bukan undefined)
# setelah build, grep bundle client:
grep -r "G-XXXXXXX" .next/static/ | headKalau string-nya muncul di .next/static/, berarti sudah ter-bake dengan benar. Kalau tidak ada, variabelnya tidak tersedia saat build.
Diagnostik 5 menit: kode atau infra?
Saat saya stuck, trik tercepat untuk memisahkan "masalah kode" vs "masalah environment" adalah hardcode sementara nilainya di staging:
// Sementara, HANYA untuk diagnosa di staging:
const gaId = process.env.NEXT_PUBLIC_GA_ID ?? "G-HARDCODED-TEST";- Kalau dengan nilai hardcode tag-nya muncul → kode Anda benar, masalahnya di penyediaan environment saat build.
- Kalau tetap tidak muncul → masalahnya di kode/komponen, bukan environment.
Setelah ketahuan, hapus hardcode-nya. Lima menit ini menghemat berjam-jam menebak.
Yang perlu diingat
NEXT_PUBLIC_*= build-time, di-bake ke bundle. Variabel tanpa prefixNEXT_PUBLIC_hanya tersedia di server (Server Component / route handler /getServerSideProps) saat runtime — itu memang aman dibaca runtime.- "Redeploy" ≠ "rebuild". Kalau platform memakai cache build, variabel baru tidak ikut. Paksa build penuh.
- Jangan menaruh secret di
NEXT_PUBLIC_*— karena di-bake ke bundle, nilainya terlihat publik di browser. Untuk secret, pakai env server tanpa prefix.
Begitu Anda paham bahwa NEXT_PUBLIC_* itu "difoto" saat build dan tidak pernah dibaca ulang di browser, bug "sudah di-set tapi undefined" jadi masuk akal — dan perbaikannya selalu sama: pastikan ada saat build, lalu build ulang yang fresh.
