Bug ini saya buat sendiri, dan saya buat dengan niat baik. Landing page pra-peluncuran ini saya bangun sebagai tema WordPress classic dengan CSS, HTML5 video, JavaScript, dan anime.js v4. Video latar hero-nya saya buat sendiri lewat image-to-video dengan Seedance 1.5 Pro lewat Higgsfield CLI, dari satu render arsitektural. Hasilnya berkas mp4 8 detik, 1080p, rasio 21:9, h264 dengan faststart, muted loop, dan cuma 1,23 MB.
Versi pertama hero-nya terasa terlalu polos. Jadi di pass berikutnya, yang saya sebut immersive pass, saya tambahkan ken-burns: animasi scale CSS pelan yang biasa dipakai supaya gambar diam terasa hidup. Di kepala saya ini murah dan aman.
Setelah itu video hero-nya terasa tersendat. Bukan frame drop yang jelas, lebih ke gerak yang tidak pernah benar-benar mulus, seolah tiap frame agak tertahan sebelum dilepas.
Dua gerakan kamera di satu elemen
Yang saya lupa: catatan projek menyebut video hasil image-to-video itu sudah punya camera motion sendiri, dan gerak itu ada di dalam framenya. Ken-burns dari CSS berarti gerakan kamera kedua ditumpuk di atas gerakan kamera pertama.
Penjelasan yang saya pegang, dan saya tekankan ini penjelasan kerja bukan hasil pengukuran, adalah browser harus me-resample tiap frame video yang sudah di-decode lewat transform yang nilainya berubah terus. Frame yang datang dari decoder tidak pernah bisa dipasang apa adanya; tiap frame lewat skala yang sedikit berbeda dari frame sebelumnya, dan gabungan dua gerakan yang tidak saling tahu itu terbaca oleh mata sebagai tersendat. Saya tidak punya angka frame-time untuk mendukung penjelasan ini, jadi saya perlakukan sebagai hipotesis yang kebetulan cocok dengan perbaikannya.
Bentuk yang salah, dengan nilai yang saya sederhanakan:
@keyframes ken-burns {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
/* ditumpuk ke video yang sudah punya camera motion sendiri */
.lp-visual video {
animation: ken-burns 14s ease-in-out infinite alternate;
}Kode ini tidak salah secara sintaks, dan di atas gambar diam dia memang kerja seperti yang diharapkan. Masalahnya ada pada elemen yang dipasangi, bukan pada animasinya.
Perbaikannya: ken-burns hanya untuk gambar diam
Aturan yang lahir dari bug ini sederhana: jangan tumpuk ken-burns atau scale CSS ke video yang framenya memang sudah bergerak sendiri. Ken-burns hanya boleh hidup di fallback gambar diam, yang di projek ini ditandai class lp-visual--still. Kalau video gagal dimuat dan yang tampil poster diamnya, gerakan halus itu tetap ada. Kalau videonya jalan, dia bergerak dengan gerakannya sendiri saja.
Dua hal lain juga dipasang di landing ini. Video diberi compositor layer sendiri lewat translateZ(0) dan will-change, supaya dia dilukis terpisah dari elemen di sekitarnya. Lalu karakter teks intro, yang dianimasikan anime.js saat hero masuk, juga di-layer-promote dengan will-change: transform, dan promosinya dilepas saat onComplete supaya layer-nya tidak hidup selamanya.
/* ken-burns pindah ke fallback gambar diam */
.lp-visual--still img {
animation: ken-burns 14s ease-in-out infinite alternate;
}
/* video dapat layer compositor sendiri, tanpa scale */
.lp-visual video {
transform: translateZ(0);
will-change: transform;
}const { animate } = window.anime;
const chars = hero.querySelectorAll('.lp-hero__char');
chars.forEach((el) => { el.style.willChange = 'transform'; });
animate(chars, {
translateY: ['105%', '0%'],
onComplete: () => {
// promosinya dilepas begitu intro selesai
chars.forEach((el) => { el.style.willChange = ''; });
},
});Catatan projek menandai perbaikan ini sudah dikonfirmasi. Saya tidak punya grafik frame-time sebelum dan sesudah, jadi saya tidak akan berpura-pura punya; yang saya punya adalah aturan yang tercatat tanggal 9 Juli 2026 supaya kesalahan yang sama tidak masuk lagi di ronde berikutnya.
Jangan mendiagnosis kehalusan dari window yang tertutup
Satu jebakan yang nyaris membuat saya salah menilai perbaikannya. Saya memverifikasi landing lewat Playwright, dan pada window automation yang tertutup atau tidak fokus, requestAnimationFrame bisa ter-throttle sampai sekitar 1 fps. Tween JavaScript kelihatan patah-patah di tes, padahal di browser yang fokus normal-normal saja.
Karena itu aturan verifikasinya saya pisah. Logika, yaitu state, urutan, dan nilai akhir, boleh diperiksa dari window mana pun. Kehalusan tidak. Sebelum menyimpulkan ada bug gerak, cek dulu rafPerSecond di window itu.
// jalankan di page.evaluate sebelum menilai kehalusan
async function rafPerSecond() {
let n = 0;
const t0 = performance.now();
await new Promise((done) => {
const tick = () => {
n += 1;
if (performance.now() - t0 < 1000) requestAnimationFrame(tick);
else done();
};
requestAnimationFrame(tick);
});
return n;
}Kalau angkanya mendekati satu, yang patah-patah itu jendelanya, bukan animasinya.
Video hasil AI tadi memang cuma pengisi slot. Pada 26 Agustus klien mengirim klip pembuka lalu master 4K lewat layanan transfer, dan hero-nya diganti webm 2560x1440 untuk landscape dan 1440x2560 untuk portrait dengan mp4 di resolusi sama sebagai fallback. Aturannya tetap sama: klip yang sudah bergerak tidak perlu dibantu bergerak.
Yang saya bawa pulang
- Ken-burns adalah alat untuk gambar diam. Di atas video yang sudah punya camera motion, dia jadi gerakan kamera kedua yang bertabrakan dengan yang pertama.
- Video latar layak dapat compositor layer sendiri, tapi layer itu untuk memisahkan lukisan, bukan alasan untuk menempelkan animasi di atasnya.
will-changepada teks intro dipasang sebelum tween dan dilepas dionComplete. Promosi layer yang tidak pernah dicabut adalah utang yang dibayar pengunjung.- Window automation yang tertutup bisa jalan di sekitar 1 fps. Ukur
rafPerSecondsebelum percaya pada mata sendiri di hasil tes.