Saya sedang membangun ulang sebuah tema WordPress dan menjalankannya di localhost. Karena iterasinya cepat, saya pasang pipeline verifikasi visual: setiap kali selesai satu section, ambil screenshot, lihat hasilnya, lanjut. Jalur utamanya browser sungguhan yang dikendalikan Playwright. Kalau jalur itu lagi rewel, ada fallback: panggil Chrome langsung dalam mode headless lewat baris perintah.
Suatu sore fallback itu mulai mengembalikan gambar yang bikin jantung berhenti sebentar. Homepage-nya hitam. Bukan hitam kosong seperti halaman gagal render, tapi hitam berlapis, jelas ada overlay gelap yang menutupi seluruh viewport. Reaksi pertama saya persis seperti yang seharusnya tidak dilakukan: saya langsung buka file tema dan mulai mencari apa yang saya rusak.
Gejalanya cuma ada di satu tempat
Sebelum mengubah apa pun, saya buka halaman yang sama di browser biasa. Homepage-nya normal. Ada intro loader gelap yang muncul sekitar sedetik, lalu memudar, lalu hero-nya tampil seperti seharusnya. Saya reload beberapa kali, hard refresh, coba profil bersih. Selalu normal.
Jadi situasinya: satu halaman, dua cara melihat, dua hasil yang berbeda total.
- Browser sungguhan lewat Playwright: bersih, loader lewat, hero kelihatan.
- Chrome headless lewat baris perintah: overlay gelap menutup semuanya, selamanya.
Itu petunjuk yang sama besarnya dengan errornya sendiri. Gejala yang tidak ikut pindah ke browser sungguhan itu properti alat penangkapnya, bukan properti situsnya.
Akar masalahnya: load menunggu si video, dan si video tidak pernah selesai
Intro loader di tema itu logikanya sederhana banget, dan sangat umum dipakai:
window.addEventListener("load", () => {
document.body.classList.add("is-loaded"); // loader fade out
});Overlay-nya mulai dari opacity: 1 dan baru turun ke 0 setelah class itu terpasang. Semuanya bergantung pada satu sinyal: event load pada window.
Dan di sinilah jebakannya. Event load bukan "DOM sudah siap". Itu DOMContentLoaded. Event load baru menyala setelah seluruh subresource halaman dianggap selesai, termasuk gambar, iframe, dan media. Hero section halaman itu punya <video> autoplay sebagai background.
Di browser sungguhan, video itu jalan normal, dianggap cukup dimuat, load menyala, loader memudar. Di Chrome headless, pipeline media-nya tidak berperilaku sama. Video-nya tidak pernah sampai ke titik yang bikin halaman dianggap selesai. load tidak pernah fire. Class is-loaded tidak pernah terpasang. Overlay tetap di opacity: 1 sampai budget waktu headless-nya habis, lalu Chrome memotret persis keadaan itu: layar gelap.
Situsnya baik-baik saja. Yang saya potret adalah keadaan yang secara praktis tidak pernah dialami pengunjung mana pun. Itu artefak headless, bukan bug.
Jadi perbaikannya juga bukan di kode. Untuk halaman yang punya hero video, saya balik ke jalur browser sungguhan, dan hasilnya memang bersih: loader lewat, hero kelihatan, tanpa satu baris pun diubah di tema. Fallback headless tetap saya simpan, tapi untuk halaman yang tampilannya tidak bergantung pada media berat.
Pelajaran yang lebih luas dari kasus ini: window load itu sinyal yang jauh lebih rapuh dari kelihatannya, karena dia menggantungkan diri pada resource paling lambat di halaman. Satu media besar yang stalling sudah cukup untuk menahan seluruh event-nya.
Perintah headless yang akhirnya saya pakai
Karena fallback-nya tetap berguna, saya rapikan perintahnya sekalian supaya hasilnya konsisten:
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--headless=new `
--hide-scrollbars `
--no-sandbox `
--user-data-dir="C:\tmp\cshotN" `
--window-size=1440,1600 `
--virtual-time-budget=9000 `
--screenshot="C:\tmp\out.png" `
URLBeberapa flag di situ bukan sekadar hiasan:
--hide-scrollbarsmenghilangkan scrollbar yang kalau tidak dimatikan ikut kepotret dan menggeser lebar layout beberapa piksel.--user-data-dirmenunjuk ke folder baru tiap kali, supaya tidak ada state profil lama yang menempel di hasil.--virtual-time-budget=9000memberi halaman ruang untuk menyelesaikan animasi dan fetch sebelum dipotret. Ini juga yang jadi batas waktu tunggu saatloadtidak pernah datang.
Untuk urusan GPU, aturannya terbelah dua:
- Halaman yang memakai WebGL: tambahkan
--use-angle=swiftshader --enable-unsafe-swiftshadersupaya canvas-nya tetap ter-render lewat software renderer. - Halaman biasa tanpa WebGL: tambahkan
--disable-gpusaja.
Jebakan kedua: halaman scroll-reveal kepotret dalam keadaan kosong
Kasus yang sama muncul lagi dengan wajah berbeda. Saya punya beberapa file HTML mockup statis untuk presentasi desain, dan screenshot full-page-nya balik hampir kosong. Section-nya ada, tingginya benar, isinya tidak kelihatan.
Penyebabnya satu keluarga dengan yang tadi. Mockup itu memakai scroll-reveal: elemen-elemennya mulai dari opacity: 0 dan baru dianimasikan masuk ketika di-scroll. Capture full-page memperbesar area yang dipotret, tapi dia tidak pernah benar-benar men-scroll halamannya. Jadi yang terekam adalah keadaan awal, yaitu semua elemen masih transparan.
Perbaikannya bukan mengubah desainnya, tapi menyuntik override sementara sebelum capture: matikan semua transisi dan animasi, lalu paksa elemen reveal-nya ke keadaan akhir.
/* sementara, hanya dipasang saat mau capture */
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
}
[data-reveal],
.reveal,
.fade-up {
opacity: 1 !important;
transform: none !important;
}Mematikan transition itu penting, bukan pelengkap. Kalau transisinya masih hidup, memaksa opacity ke 1 justru memulai animasi baru, dan screenshot-nya bisa mendarat di tengah-tengah fade dengan konten setengah pudar. Dimatikan dulu, baru dipaksa, hasilnya deterministik.
Yang saya bawa pulang
- Kalau anomali cuma muncul di alat otomasi dan hilang di browser sungguhan, jangan langsung tambal situsnya. Buktikan dulu itu bug atau artefak.
- Loader yang menunggu
windowloadakan ikut mati bersama resource paling lambat di halaman. Background video adalah kandidat paling sering. - Screenshot headless itu bukan foto pengalaman pengguna. Dia foto keadaan halaman pada satu titik waktu buatan, dengan pipeline media dan GPU yang berbeda.
- Sebelum memotret halaman yang animasinya dipicu scroll, matikan transisi dan paksa keadaan akhirnya. Capture tidak pernah men-scroll untuk kamu.
Yang paling mahal di kasus ini bukan waktunya, tapi seberapa dekat saya dengan mengirim "perbaikan" untuk masalah yang tidak pernah dialami satu pengunjung pun. Kalau saya menuruti screenshot pertama itu, saya kemungkinan besar akan membongkar logika loader yang sebenarnya sehat, demi memuaskan sebuah browser yang tidak pernah bisa memutar video-nya.