Ada satu situs klien berbasis WordPress dengan tema custom yang saya pegang, hosting di Hostinger dengan LiteSpeed dan CDN menyala. Dalam beberapa hari saya kena dua insiden yang kelihatannya tidak berhubungan, tapi ternyata akar masalahnya identik. Dua-duanya sempat bikin saya curiga pada hal yang salah lebih dulu.
Insiden pertama muncul saat saya iterasi video hero. Saya re-encode hero.webm berkali-kali, upload ulang dengan nama file yang sama persis, refresh browser, dan yang main tetap potongan lama. Saya sampai bilang ke diri sendiri "ini belum kepotong" lebih sering dari yang mau saya akui, balik ke editor, encode lagi, upload lagi, hasilnya sama. Sempat saya kira proses encoding-nya yang gagal.
Insiden kedua datang dari klien. Satu foto yang saya ganti dilaporkan "tidak muncul". Kali ini saya tidak buru-buru menyalahkan file. Saya periksa dulu apa yang benar-benar dikirim server.
Menelusuri jejaknya
Saya cek header respons foto itu langsung dari server, lewat CDN:
curl -I https://situs-klien.example/wp-content/themes/tema/assets/img/galeri-01.webpHasilnya justru menunjukkan server dalam kondisi sehat:
HTTP/2 200
last-modified: Tue, 15 Jul 2026 09:12:44 GMT
content-length: 205431
cache-control: max-age=604800
x-hcdn-cache-status: MISSTiga baris ini yang penting. last-modified hari yang sama saat saya upload, content-length 205KB cocok dengan foto baru, dan x-hcdn-cache-status: MISS artinya CDN tidak menyajikan dari cache-nya sendiri, dia baru saja mengambil versi segar dari origin. Dari sisi server, seratus persen file baru. Tapi di browser saya dan di browser klien, tetap foto lama.
Akar masalahnya
Biang keroknya ada di satu baris yang tadinya saya anggap sepele: cache-control: max-age=604800. Itu tujuh hari. Artinya, browser siapa pun yang pernah membuka URL foto itu sebelum saya swap sudah menyimpan salinannya dan menganggapnya segar selama tujuh hari penuh. Selama jendela itu, browser tidak akan repot bertanya lagi ke server. URL-nya sama persis, nama file-nya sama persis, jadi dari kacamata browser tidak ada alasan untuk re-request.
Di sinilah letak jebakan swap nama sama: server boleh pegang file paling baru, CDN boleh bilang MISS dan segar, tapi kalau browser masih pegang salinan yang belum kedaluwarsa untuk URL yang identik, dia tidak akan pernah melihat file baru itu. Dan ini berlaku sama untuk img src maupun video src. Video hero saya kena tembok yang persis sama: hero.webm dengan nama tetap, di-cache tujuh hari, jadi setiap upload ulang tak terlihat oleh browser yang sudah pernah memuatnya.
Ini juga menjelaskan kenapa saya yang paling parah kena. Saya returning visitor terberat di situs itu, jadi cache saya paling penuh. Buka di incognito, foto dan video baru langsung muncul, karena incognito tidak punya cache lama untuk URL itu. Itu konfirmasi terakhir: ini bukan file, ini cache browser.
Perbaikannya
Solusinya bukan mematikan cache. max-age tujuh hari itu justru bagus untuk performa, dan saya tidak mau membuangnya. Yang saya butuhkan adalah cara agar URL berubah setiap kali file-nya berubah, tapi tetap stabil selama file-nya sama. Browser meng-cache berdasarkan URL lengkap termasuk query string, jadi menempelkan query versi yang terikat ke waktu modifikasi file menyelesaikan dua-duanya sekaligus.
Untuk video hero, yang paling mendesak dan wajib, saya tempelkan query ?v=<filemtime> ke src-nya:
<video src="<?php echo esc_url( $video_url . '?v=' . filemtime( $video_path ) ); ?>" muted playsinline></video>filemtime() mengembalikan timestamp modifikasi file. Setiap kali saya re-encode dan upload ulang hero.webm, timestamp-nya berubah, query-nya berubah, URL-nya berubah, dan browser terpaksa mengambil ulang. Tanpa query ini, browser akan terus menyajikan video lama, dan itu bukan teori, itu persis yang saya alami.
Lalu saya generalisasi jadi satu helper untuk semua gambar tema, biar tidak perlu mikir lagi tiap kali ganti foto:
function asset_img( $file ) {
$path = get_theme_file_path( "assets/img/$file" );
$url = get_theme_file_uri( "assets/img/$file" );
return $url . '?v=' . filemtime( $path );
}Helper asset_img() ini saya pakai di mana-mana: array media dan compare di front page, hero dan split di page block, sampai template layanan dan portfolio. Sekarang setiap swap foto dengan nama sama otomatis membawa ?v= baru dan langsung segar. Saya verifikasi di staging, semua foto baru termuat dengan versi segar seperti ?v=1782899303.
Satu pengecualian yang saya buat sengaja: logo. Logo hampir tidak pernah berubah, jadi saya biarkan tanpa cache-bust supaya tetap di-cache penuh. Cache-busting itu untuk aset yang memang sering diganti, bukan untuk semuanya.
Waktu QA, biar tidak bolak-balik hard reload manual, saya pakai snippet kecil di console lewat Playwright untuk memaksa semua gambar mengambil ulang:
[...document.images].forEach(i => {
const u = new URL(i.src);
u.searchParams.set('v', 'x');
i.src = u;
});Pelajaran
- Kalau file sudah benar di server tapi browser tetap menampilkan versi lama, curigai cache browser, bukan file atau CDN.
- Baca header respons:
cache-control: max-ageyang besar plusx-hcdn-cache-status: MISSberarti server segar tapi browser menahan salinan lama. - Swap file dengan nama yang sama tidak akan terlihat oleh siapa pun yang pernah memuat URL itu selama
max-agebelum lewat. Berlaku untukimgsrc danvideosrc. - Jangan matikan caching demi menyelesaikan ini. Pakai query versi terikat
filemtime()supaya URL berubah saat file berubah, tapi tetap stabil dan tetap ter-cache saat file sama. - Bungkus jadi satu helper dan pakai di semua aset yang bisa diganti, jadi swap berikutnya otomatis segar tanpa kamu perlu ingat.
- Kecualikan aset yang nyaris tak pernah berubah seperti logo, biar tetap menikmati cache penuh.
