Kembali ke blog
13 Agustus 2026Sergei Solod18 mnt baca

Mengapa Tab Next.js Lama Rusak Setelah Deployment: HTML Usang, Chunk Hilang, dan Ketidakselarasan Versi

Setelah sebuah deployment, pemantauan produksi saya merekam kegagalan memuat chunk Next.js milik aplikasi sendiri. Log itu membuktikan kegagalannya, bukan penyebabnya. Artikel ini memakai insiden tersebut untuk membahas tab lama, HTML usang, aset /_next/static yang hilang, ketidakselarasan versi, retensi, deploymentId, urutan rollout, pemantauan, dan pemulihan terkontrol.

Next.jsDeploymentKetidakselarasan VersiCache WebKeandalan FrontendAset statis

Salah satu error produksi paling berguna yang saya lihat setelah deployment tampak hampir biasa saja:

Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js

Error itu muncul di aliran yang sama dengan kegagalan analytics, script iklan, pesan generik seperti Script error., dan playback video yang terputus. Sebagian besar kejadian tersebut hanyalah derau. Yang ini berbeda: sumber daya yang gagal dimuat adalah milik aplikasi Next.js saya sendiri. Jika browser memang benar-benar tidak bisa memuatnya, sebagian halaman bisa berhenti bekerja.

Yang tidak saya ketahui dari log tersebut adalah penyebabnya. Bisa saja ada masalah jaringan sementara, kegagalan pada proxy atau CDN, file memang tidak ada, atau sebuah halaman lama masih meminta chunk dari deployment sebelumnya setelah server sudah beralih ke build yang baru.

Skenario terakhir mudah diremehkan karena deployment terbaru bisa sepenuhnya sehat. Semua pengunjung baru membuka versi terbaru tanpa masalah, sementara sebuah tab yang sudah terbuka berjam-jam secara praktis masih merupakan klien dari versi lama.

Artikel ini membahas celah kompatibilitas itu: mengapa tab Next.js lama bisa rusak setelah deployment, bagaimana HTML usang dan aset /_next/static yang sudah dihapus menimbulkan ketidakselarasan versi, mengapa pembersihan hasil build yang terlalu agresif memperburuk masalah, dan bagaimana saya akan merancang retensi, deployment, pemantauan, serta pemulihan agar rilis yang sukses tidak memutus pengguna yang sudah berada di dalam aplikasi.

Pelajaran pertama: tidak semua kegagalan script adalah masalah deployment

Aliran error awal berisi beberapa kelas masalah yang sama sekali berbeda. Script analytics dan iklan pihak ketiga bisa diblokir oleh content blocker, filter DNS, fitur privasi, antivirus, pembatasan regional, atau jaringan pengguna. Promise play() pada video bisa dibatalkan oleh pause() yang dipanggil kemudian tanpa berarti aplikasi rusak. Pesan lintas origin seperti Script error. sering kali juga terlalu minim informasi untuk didiagnosis.

Kegagalan chunk Next.js milik aplikasi sendiri harus diberi prioritas berbeda. Batas yang berguna bukan “ada error JavaScript atau tidak”, melainkan lebih seperti:

sumber daya pihak ketiga gagal
    -> biasanya telemetri atau fitur opsional yang terganggu

/_next/static/*.js milik aplikasi gagal
    -> kode aplikasi yang dibutuhkan mungkin tidak tersedia

Pemisahan ini penting karena reporter yang terlalu sensitif bisa menenggelamkan kejadian serius di antara ratusan pesan yang tidak relevan. Dalam kasus saya, sumber daya yang patut diperhatikan adalah /_next/static/chunks/9253.647385b4be0958e4.js. Log membuktikan bahwa sebuah script first-party gagal dimuat. Log itu tidak membuktikan bahwa penyebabnya adalah ketidakselarasan versi akibat deployment.

Saya sengaja menjaga batas pembuktian itu: penyebab yang masuk akal belum tentu penyebab yang sudah terbukti.

Tab browser yang masih terbuka pada dasarnya adalah klien dari rilis lama

Model mental yang membuat masalah ini lebih jelas bagi saya cukup sederhana: setelah deployment, lebih dari satu versi aplikasi bisa tetap hidup pada saat yang sama.

Misalnya rilis A aktif pukul 10.00. Seorang pengguna membuka halaman dan menerima HTML serta JavaScript yang dibutuhkan untuk route itu. Pukul 10.30 rilis B dipublikasikan. Pengunjung baru menerima B. Namun tab yang sudah terbuka tidak otomatis berubah menjadi B hanya karena server sudah berganti.

Tab tersebut masih bisa menyimpan:

  • runtime JavaScript dari rilis A;
  • referensi route dan chunk yang dibuat A;
  • data navigasi yang sudah diprefetch oleh A;
  • state React yang dibuat saat A berjalan;
  • modul hasil code splitting dari A yang sudah diunduh;
  • referensi ke modul A yang belum pernah diunduh.

Poin terakhir biasanya menjadi tempat masalah mulai terlihat.

Jika semua chunk yang nantinya dibutuhkan pengguna sudah ada di cache browser, sesi bisa terus berjalan tanpa gejala. Tetapi Next.js membagi kode. Perpindahan route, dynamic import, modal, editor, atau fitur yang baru digunakan belakangan dapat memerlukan file JavaScript tambahan. Runtime lama lalu meminta URL yang valid pada rilis A.

Jika server masih menyimpan aset itu, pengguna mungkin tidak merasakan apa pun. Jika deployment sudah menghapusnya, klien lama dapat menerima 404 walaupun rilis B sepenuhnya sehat untuk pengguna baru.

Chunk dengan hash konten memang dirancang untuk cache jangka panjang

Next.js sengaja memberi cache jangka panjang pada aset yang benar-benar immutable. Dokumentasi self-hosting saat ini menyatakan bahwa aset immutable dengan hash SHA pada nama file disajikan dengan kebijakan satu tahun seperti berikut:

Cache-Control: public, max-age=31536000, immutable

Itu masuk akal: ketika isi berubah, URL berubah. Browser tidak perlu memvalidasi ulang file yang sama pada setiap permintaan.

Konsekuensi pentingnya adalah: URL lama tetap bermakna selama masih ada dokumen atau runtime lama yang bisa merujuk kepadanya.

Cache satu tahun tidak membantu jika browser belum pernah mengunduh chunk tertentu sebelum deployment, lalu baru memintanya setelah origin sudah menghapus file tersebut.

Karena itu, “aset statis kami immutable” tidak sama dengan “aset dari rilis sebelumnya boleh langsung dihapus”. Sifat immutable membuat aset lama aman untuk dipertahankan. Sifat itu tidak membuat klien lama berhenti memintanya.

Panduan self-hosting Next.js saat ini secara eksplisit menyebut JavaScript atau CSS yang hilang sebagai salah satu gejala ketidakselarasan versi pada multi-server atau deployment bertahap. Mekanisme dasarnya tetap sama ketika skew terjadi antara tab lama dan origin yang sudah menjalankan versi baru.

Versi bisa terpisah melalui beberapa mekanisme berbeda

Menyebut semuanya “masalah cache” terlalu samar. Saya memisahkan setidaknya empat mekanisme karena masing-masing membutuhkan perbaikan yang berbeda.

1. Tab lama meminta aset yang belum pernah dimuat sebelum deployment

Ini adalah kasus klasik sesi panjang. Dokumen dan runtime berasal dari A. B menggantikan file di server. Belakangan pengguna membuka fitur yang membutuhkan lazy chunk dari A. Jika chunk A itu sudah dihapus, permintaan gagal.

2. HTML usang menunjuk ke chunk yang sudah tidak ada

CDN, reverse proxy, Service Worker, cache browser, atau lapisan static hosting dapat mempertahankan HTML lama lebih lama dari yang diperkirakan. HTML itu masih menunjuk ke aset A sementara origin hanya memiliki B.

Masalah menjadi lebih berbahaya jika HTML secara tidak sengaja diberi kebijakan immutable panjang yang sama seperti file ber-hash. Chunk dapat disimpan lama karena URL-nya sudah memversioning konten. HTML justru merupakan objek yang menentukan kumpulan URL mana yang harus bekerja bersama.

3. Deployment bertahap atau beberapa instance menyajikan rilis berbeda

Bayangkan dua instance Next.js di belakang load balancer. Satu sudah berada di B, satu lagi masih di A. Dokumen dapat berasal dari satu versi sementara navigasi berikutnya masuk ke versi lain. Dokumentasi Next.js saat ini menyebut kondisi ini ketidakselarasan versi dan mencantumkan missing aset, ketidakcocokan Server Functions, serta kegagalan navigasi sebagai kemungkinan akibatnya.

Dasar yang lebih aman adalah membangun sekali dan menjalankan build artifact yang sama pada semua instance dalam satu deployment logis. Dokumentasi self-hosting Next.js juga menyarankan build yang sama serta build ID yang konsisten di seluruh container, alih-alih menjalankan build independen di tiap replika.

4. Deployment memublikasikan file dalam urutan yang salah

Bahkan tanpa tab lama, upload yang tidak atomik dapat membuat keadaan sementara yang mustahil:

HTML baru sudah terlihat
+
chunk baru belum tersedia

atau sebaliknya:

HTML lama masih terlihat
+
chunk lama sudah dihapus

Jendela waktunya tidak perlu lama. Seorang pengguna hanya perlu masuk pada momen yang salah sekali saja.

Pola berbahaya adalah mengganti semuanya lalu menghapus struktur direktori lama

Script deployment sederhana sering tumbuh dari pola seperti:

build
rsync --delete new-output/ production/
restart

Pola itu menarik karena direktori produksi selalu persis sama dengan build terbaru. Namun pola tersebut tidak ramah terhadap klien yang berumur panjang.

Pada aset ber-hash, file lama biasanya tidak bertabrakan dengan file baru karena URL-nya berbeda. Menghapusnya segera terutama hanya menghemat storage. Sebaliknya, setiap referensi lama yang masih sah pada klien lama berubah menjadi potensi 404.

Sekarang saya melihat chunk lama sebagai material kompatibilitas deployment, bukan sampah.

Itu bukan berarti setiap rilis harus disimpan selamanya. Artinya, cleanup harus menjadi kebijakan retensi tersendiri, bukan efek samping dari proses mempromosikan rilis terbaru.

Retensi membantu, tetapi tidak ada jendela waktu terbatas yang bisa menjadi solusi sempurna

Dalam self-hosting, aset lama di /_next/static dapat dipertahankan selama grace period tertentu. Durasi yang tepat tergantung workload. Situs yang dibuka dua menit berbeda dari aplikasi yang bisa dibiarkan terbuka sepanjang hari kerja.

Cara berguna untuk memikirkan batas minimumnya:

jendela retensi >=
    usia maksimum HTML yang mungkin masih tersaji
    + durasi realistis tab yang tetap terbuka
    + jendela rollback
    + margin propagasi deployment

Ini bukan jaminan matematis. Tab bisa tetap terbuka berminggu-minggu. Tidak ada jumlah jam yang benar-benar menghilangkan kemungkinan error.

Karena itu saya lebih memilih desain berlapis:

  1. pertahankan aset immutable dari rilis sebelumnya cukup lama agar sesi normal tetap berjalan;
  2. deteksi ketidakselarasan versi agar klien dapat pindah ke rilis saat ini;
  3. sediakan muat ulang satu kali yang aman atau jalur pemulihan yang terlihat ketika aset benar-benar sudah tidak ada;
  4. pantau kegagalan chunk first-party agar retensi bisa disesuaikan berdasarkan bukti nyata.

Retensi mencegah sebagian besar kegagalan. Pemulihan menangani ekor panjang kasus yang tidak mungkin dihapus sepenuhnya oleh jendela waktu terbatas.

Jangan menghapus chunk lama hanya berdasarkan umur file

Aturan sederhana seperti “hapus semua file yang lebih tua dari tujuh hari” juga bisa salah. Rilis terbaru dapat memakai ulang file hash lama yang isinya tidak berubah, sehingga mtime-nya tua walaupun file masih dibutuhkan.

Garbage collection yang lebih kuat memahami rilis:

  1. simpan manifest atau daftar aset dari semua rilis yang masih berada dalam compatibility window;
  2. buat gabungan semua path yang dirujuk oleh rilis tersebut;
  3. jangan hapus apa pun yang termasuk himpunan terlindungi itu;
  4. hapus hanya sumber daya yang tidak lagi direferensikan setelah grace period tambahan.

Jika mekanisme itu terlalu rumit untuk deployment kecil, direktori static yang sedikit lebih besar sering jauh lebih murah daripada mencari penyebab error klien langka di produksi.

Aturan yang akan saya hindari sederhana: jangan menjadikan --delete pada struktur direktori /_next/static bersama sebagai bagian dari langkah yang sama ketika rilis baru dipromosikan.

Next.js sekarang punya perlindungan ketidakselarasan versi, tetapi itu bukan penyimpanan aset lama

Next.js saat ini mendukung deploymentId untuk menangani perbedaan versi saat deployment. Contohnya:

// next.config.js
const nextConfig = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
}

module.exports = nextConfig

Menurut dokumentasi deploymentId saat ini, jika dikonfigurasi Next.js menambahkan parameter ?dpl=<deploymentId> ke URL aset statis yang dikelola framework, membawa informasi deployment pada permintaan navigasi klien, dan menandai identitas deployment-nya dalam respons server. Jika ketidakselarasan terdeteksi saat navigasi, Next.js dapat melakukan navigasi penuh alih-alih meneruskan transisi sisi klien dengan data yang tidak kompatibel.

?dpl=<deploymentId>
x-deployment-id
x-nextjs-deployment-id
data-dpl-id

Fitur ini berguna, tetapi jangan memberinya kemampuan yang sebenarnya tidak ada. Dokumentasi menjelaskan bahwa Next.js tidak memakai parameter masuk ?dpl= untuk merutekan permintaan ke deployment lama. Parameter itu digunakan untuk cache busting. Jika origin self-hosted sudah benar-benar menghapus file lama, query parameter tidak akan menghidupkannya kembali.

Karena itu saya memperlakukan deploymentId sebagai mekanisme deteksi dan pemulihan, bukan pengganti retensi aset atau deployment hygiene.

Platform hosting tertentu dapat melakukan lebih jauh. Dokumentasi Skew Protection Vercel saat ini, misalnya, menjelaskan version locking agar permintaan yang dikelola framework terus diarahkan ke deployment yang melayani klien semula. Itu adalah kemampuan platform, bukan sesuatu yang otomatis tersedia pada Nginx atau CDN biasa.

Build ID dan deployment ID berkaitan, tetapi bukan hal yang sama

Next.js juga menghasilkan build ID saat next build. Jika beberapa container seharusnya melayani deployment yang sama, mereka tidak boleh diam-diam menjadi build yang berbeda hanya karena masing-masing menjalankan build sendiri.

Build ID deterministik dapat dikaitkan dengan identitas rilis seperti commit Git:

// next.config.js
const nextConfig = {
  generateBuildId: async () => process.env.GIT_SHA,
  deploymentId: process.env.GIT_SHA,
}

module.exports = nextConfig

Contoh ini hanya ilustrasi, bukan salinan konfigurasi produksi saya. Prinsip arsitekturnya lebih penting: satu rilis logis sebaiknya memiliki satu build artifact yang koheren dan satu identitas deployment yang konsisten pada semua instance yang melayaninya.

generateBuildId mengidentifikasi build Next.js. deploymentId didokumentasikan khusus untuk ketidakselarasan versi protection dan cache busting. Memperlakukan keduanya sebagai sinonim justru menyulitkan debugging.

Saya akan memublikasikan aset sebelum mengalihkan dokumen ke rilis baru

Urutan deployment yang aman sengaja tidak simetris. Aset immutable baru boleh tersedia sebelum ada yang merujuk kepadanya. HTML baru tidak boleh muncul sebelum aset yang dirujuk benar-benar tersedia.

Secara konseptual, urutan yang saya inginkan:

1. build rilis B satu kali
2. unggah aset B ke /_next/static
3. verifikasi aset yang dibutuhkan benar-benar dapat diunduh
4. siapkan server atau runtime B
5. jalankan health check
6. alihkan permintaan dokumen baru ke B secara atomik
7. biarkan aset statis A tetap tersedia
8. pantau B
9. bersihkan aset lama kemudian

Untuk static export, prinsipnya sama: publish aset berversi terlebih dahulu, lalu HTML yang merujuk kepadanya. Untuk SSR di belakang reverse proxy, siapkan dan periksa server baru sebelum mengalihkan traffic.

Rollback sebaiknya simetris. Menyimpan direktori rilis sebelumnya beserta aset-nya membuat rollback mungkin tanpa mencoba merekonstruksi file lama di bawah tekanan.

Namun ini tidak membuat setiap rollback otomatis aman. Migrasi database yang tidak kompatibel atau perubahan kontrak API bisa membuat aplikasi lama tidak berfungsi meskipun JavaScript-nya masih ada. Retensi aset hanya menyelesaikan bagian statis dari kompatibilitas rilis.

Direktori aset immutable bersama cocok untuk self-hosting sederhana

Untuk deployment kecil berbasis Nginx, salah satu pola sederhana adalah memisahkan rilis aplikasi aktif dari penyimpanan aset bersama.

Struktur ilustratif:

/srv/app/releases/2026-08-13-a/
/srv/app/releases/2026-08-13-b/
/srv/app/current -> /srv/app/releases/2026-08-13-b/

/srv/app/shared/_next/static/...

Setiap deployment menambahkan file /_next/static baru ke direktori bersama tanpa menghapus file dari rilis yang masih berada dalam periode retensi. Nginx dapat menyajikan path tersebut dengan kebijakan immutable:

location ^~ /_next/static/ {
    root /srv/app/shared;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Konfigurasi ini hanya contoh, bukan klaim tentang konfigurasi Nginx persis yang saya gunakan. Deployment nyata harus mempertimbangkan permission, MIME type, varian kompresi, perilaku CDN, dan struktur output yang sebenarnya.

Yang penting adalah siklus hidupnya: pointer yang mutable menuju rilis aktif dan storage yang sebagian besar append-only untuk aset ber-hash memiliki kebutuhan yang berbeda.

HTML membutuhkan kebijakan cache yang berbeda dari chunk ber-hash

Salah satu cara termudah membuat bug ini sendiri adalah menyimpan HTML di cache seolah-olah HTML juga merupakan aset immutable yang sudah berversi melalui namanya.

Pada halaman dinamis, Next.js biasanya menggunakan semantik yang mencegah reuse untuk output dinamis yang spesifik pengguna. Halaman statis dan ISR memiliki aturan lain dan dapat dikache dengan benar oleh CDN. Pada static export yang disajikan oleh Nginx, perilakunya bahkan lebih bergantung pada header yang kita tetapkan sendiri.

Karena itu saya tidak memakai satu aturan cache untuk seluruh “website”. Saya memisahkan kelas object:

aset ber-hash di /_next/static
    max-age panjang
    immutable
    aman dipertahankan

HTML / dokumen rute
    harus bisa berpindah ke rilis baru
    kebijakan tergantung model rendering
    tidak boleh hidup lebih lama dari aset yang dirujuk

RSC / navigasi / data API
    aturan kesegaran dan kompatibilitas tersendiri

Jika ada CDN, invalidasi pada route dokumen mungkin diperlukan setelah deployment, tergantung desain caching. Sebaliknya, mem-purge chunk ber-hash lama hanya karena ada rilis baru sering kontraproduktif: jika origin juga sudah menghapusnya, kita justru menghapus salinan terakhir yang mungkin masih dapat menyelamatkan tab lama.

Panduan CDN caching Next.js berguna di sini karena memisahkan caching halaman dari kebijakan jangka panjang untuk aset /_next/static.

Muat ulang otomatis adalah alat pemulihan, bukan strategi deployment utama

Respons umum ketika chunk gagal adalah “muat ulang saja”. Sering kali ini berhasil karena hard navigation mengambil dokumen saat ini, yang merujuk ke build saat ini.

Tetapi muat ulang pada setiap script error menciptakan masalah baru:

  • kegagalan script pihak ketiga dapat memicu muat ulang yang sia-sia;
  • server outage yang nyata dapat membuat loop muat ulang tak berujung;
  • form yang belum disimpan dapat kehilangan isi;
  • state lokal React hilang pada hard navigation;
  • jika rilis saat ini memang rusak, error akan berulang setelah muat ulang.

Dokumentasi Next.js saat ini juga memperingatkan bahwa hard navigation untuk pemulihan ketidakselarasan versi dapat menghilangkan state komponen seperti useState, sedangkan state yang disimpan di URL atau storage persisten dapat bertahan.

Jika saya menambahkan pemulihan di klien, saya ingin cakupannya sempit dan hanya satu kali. Contoh ilustratif:

const RECOVERY_KEY = 'next-chunk-recovery-attempted'

function isOwnNextAsset(url: string) {
  try {
    const parsed = new URL(url, window.location.href)
    return (
      parsed.origin === window.location.origin &&
      parsed.pathname.startsWith('/_next/static/')
    )
  } catch {
    return false
  }
}

window.addEventListener(
  'error',
  (event) => {
    const target = event.target
    if (!(target instanceof HTMLScriptElement)) return
    if (!isOwnNextAsset(target.src)) return

    reportChunkFailure({
      page: window.location.href,
      asset: target.src,
    })

    if (sessionStorage.getItem(RECOVERY_KEY)) return

    sessionStorage.setItem(RECOVERY_KEY, '1')
    window.location.reload()
  },
  true,
)

Ini sengaja hanya contoh. Implementasi produksi juga perlu mempertimbangkan CSS chunk, pola error framework yang dikenal, alur kerja yang bisa rusak karena muat ulang, dan cara menghapus marker pemulihan setelah load yang sehat.

Pada editor, checkout, atau form panjang, saya mungkin lebih memilih banner “Versi baru tersedia; simpan pekerjaan lalu muat ulang” daripada refresh paksa.

Payload pemantauan harus membantu membedakan ketidakselarasan versi dari kegagalan lain

Pesan “failed to load script” saja tidak cukup. Untuk membedakan chunk lama yang sudah dihapus dari gangguan jaringan acak, saya membutuhkan konteks deployment.

Field yang berguna antara lain:

  • URL aset yang gagal;
  • URL halaman saat ini;
  • apakah sumber daya first-party atau third-party;
  • rilis atau deployment ID yang terlihat oleh klien;
  • browser dan sistem operasi;
  • navigator.onLine sebagai sinyal lemah, bukan bukti konektivitas;
  • waktu sejak halaman dibuka;
  • apakah error terjadi dekat dengan waktu deployment;
  • apakah pemulihan sudah pernah dicoba;
  • status HTTP ketika dapat diamati dari sisi server;
  • rilis yang benar-benar melayani permintaan pada origin atau proxy.

Setelah itu polanya jauh lebih berarti.

Jika banyak pengguna dari jaringan berbeda meminta URL hash lama tepat setelah deployment dan origin mengembalikan 404, penghapusan aset terlalu cepat menjadi penjelasan yang kuat. Jika hanya satu pengguna mengalami kegagalan level jaringan tanpa respons HTTP, ketidakselarasan versi jauh lebih tidak pasti. Jika chunk mengembalikan 200 tetapi isinya HTML atau MIME type salah, masalahnya lebih mungkin routing atau proxy.

Saya juga akan memisahkan alert chunk first-party dari error sumber daya pihak ketiga. Itulah perubahan pemantauan yang paling langsung didukung oleh log awal saya: sinyal penting tercampur dengan banyak derau browser.

Tes reproduksi sederhana, tetapi tab lama harus benar-benar dibiarkan lama

Kelas bug ini mudah lolos dari testing rilis biasa karena developer cenderung langsung refresh setelah deployment. Refresh justru menghancurkan kondisi yang sedang ingin diuji.

Tes manual yang lebih baik:

  1. deploy rilis A;
  2. buka tab dengan perilaku cache yang realistis;
  3. gunakan hanya sebagian aplikasi agar beberapa route atau fitur lazy belum dimuat;
  4. biarkan tab terbuka;
  5. deploy rilis B;
  6. jangan refresh tab lama;
  7. buka route atau fitur dinamis yang membutuhkan kode yang belum pernah diunduh;
  8. periksa Network dan Console;
  9. verifikasi apakah URL aset lama masih mengembalikan 200;
  10. verifikasi apakah deteksi ketidakselarasan versi menghasilkan hard navigation yang terkontrol saat diperlukan.

Saya akan mengulang tes yang sama dengan CDN, dengan beberapa instance selama deployment bertahap, dan setelah jendela retensi berakhir.

Satu kesalahan testing yang halus adalah menyalakan “Disable cache” di DevTools untuk seluruh skenario. Itu berguna untuk beberapa diagnosis, tetapi mengubah sistem yang sedang diuji. Kasus tab lama juga harus diuji dengan caching realistis.

Tidak semua kegagalan chunk diselesaikan dengan menyimpan file lama

Retensi berguna justru karena ia menyelesaikan satu mekanisme spesifik. Ia tidak boleh menjadi penjelasan universal baru.

Chunk first-party dapat gagal karena:

  • permintaan tidak pernah mencapai server;
  • koneksi terputus;
  • extension browser memblokirnya;
  • edge CDN mengalami gangguan sementara;
  • Nginx salah merutekan path;
  • server mengembalikan halaman error HTML alih-alih JavaScript;
  • kompresi atau Content-Encoding rusak;
  • permission file salah;
  • deployment parsial tidak pernah mengupload chunk;
  • file sebenarnya ada tetapi dihapus terlalu cepat;
  • klien dan server berada pada deployment yang tidak kompatibel.

Status respons dan waktunya sangat penting. 404 berulang pada URL hash lama setiap selesai rilis bercerita sangat berbeda dari ERR_CONNECTION_RESET pada satu jaringan seluler.

Karena itu saya tidak akan menulis ulang insiden awal sebagai “saya membuktikan HTML usang merusak situs”. Saya tidak membuktikannya. Yang saya lihat adalah kegagalan chunk first-party yang nyata, lalu dari sana saya mengidentifikasi ketidakselarasan versi sebagai failure mode serius yang layak dicegah lewat desain.

Deployment yang aman memperlakukan klien lama sebagai bagian dari permukaan rilis

Kesalahan mental yang lebih dalam adalah menganggap deployment menggantikan A dengan B pada satu titik waktu.

Di server, symlink atau orchestrator mungkin membuatnya terlihat demikian. Dalam sistem penuh, object A bisa masih ada di CDN, dokumen A masih berjalan di browser, A dan B dapat hidup bersamaan selama rollout, dan A mungkin menjadi aktif lagi setelah rollback.

Jadi permukaan rilis yang nyata adalah sebuah interval waktu, bukan satu titik.

Aturan deployment Next.js saya sekarang dibangun dari model itu:

  • Build satu kali per rilis logis. Replika tidak boleh diam-diam menghasilkan output berbeda.
  • Publish aset immutable sebelum dokumen yang merujuk kepadanya.
  • Pertahankan aset hash lama selama compatibility window yang disengaja.
  • Jangan berikan kebijakan cache yang sama pada HTML mutable dan chunk ber-hash.
  • Gunakan deploymentId ketika model deployment dapat mengalami ketidakselarasan versi.
  • Gunakan skew protection milik platform hanya jika hosting memang menyediakan routing sadar versi.
  • Buat pemulihan satu kali dan sadar terhadap state pengguna.
  • Monitor kegagalan chunk first-party sebagai sinyal produksi terpisah.
  • Uji deployment dengan tab lama yang masih terbuka.
  • Lakukan garbage collection aset lama belakangan, bukan saat promosi rilis.

Aturan yang saya gunakan sekarang

Build yang hijau dan halaman baru yang sehat tidak membuktikan bahwa deployment aman bagi pengguna yang sudah membuka aplikasi sebelumnya.

Tab lama bukan sisa yang boleh diabaikan. Ia adalah klien nyata yang menjalankan rilis nyata sebelumnya.

Setelah saya mulai melihat deployment dengan cara ini, masalah chunk menjadi jauh kurang misterius. Hash konten memberi sebuah aset identitas yang stabil. Cache jangka panjang membuat identitas itu efisien. Tetapi deployment harus menghormati identitas tersebut cukup lama, atau memberi klien jalan terkontrol untuk maju ke versi terbaru.

Saya tidak perlu mempertahankan setiap rilis lama selamanya. Saya hanya perlu sistem yang mampu melewati masa ketika klien lama dan server baru secara sah hidup bersamaan.

Itulah kontrak deployment yang saya pedulikan sekarang: pengguna baru mendapatkan rilis baru, pengguna lama tidak kehilangan file yang masih diketahui oleh rilis yang sedang mereka jalankan, dan setiap ketidakcocokan yang tersisa berakhir pada pemulihan yang disengaja, bukan halaman yang rusak.