Keluhan yang masuk di ronde feedback ke-13, tertanggal 24 September 2026, spesifik sekali: background buatan desainer tampak cukup blur atau pecah di situs, terutama kalau situsnya dilihat di layar yang lebih besar. Klien menambahkan bahwa dia tidak mengerti kenapa, karena desainernya membuat background itu dalam resolusi tinggi, dan berkas HD aslinya sudah dikirim lewat WeTransfer. Permintaannya juga jelas. Periksa bagaimana berkas-berkas itu dikompres, diubah ukurannya, atau dirender di situs, karena klien ingin hasilnya tetap setajam dan sedetail berkas aslinya.
Klien benar, dan penyebabnya ada di pihak saya, bukan di berkas desainer. Sebabnya ada dua, dan keduanya ketemu dengan mengukur, bukan dengan memandangi layar. Yang pertama urusan CSS, yang kedua urusan encoder.
cover menghitung dari section, bukan dari layar
Background di situs ini digambar langsung di section-nya. Aturan dasarnya seperti ini, dengan nama kelas dan custom property yang saya samarkan:
.ground {
position: relative;
background-color: var(--section-base);
background-image: var(--ground);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}Untuk section yang kira-kira setinggi satu layar, aturan ini benar. Masalahnya, cover menskalakan gambar ke kotak section, bukan ke layar, dan beberapa section di situs ini tingginya sekitar 3.800px: section di dua halaman yang panjang, dan satu section di beranda. Di section setinggi beberapa layar, gambarnya ikut diperbesar menurut tinggi section.
Skalanya bisa dihitung per section. cover memilih faktor terbesar dari dua sumbu supaya kotaknya tertutup penuh, lalu layar dengan DPR di atas 1 mengalikannya lagi:
skala = max(lebar section / lebar gambar, tinggi section / tinggi gambar) × DPRSemua background dari desainer berukuran 1920x1440. Di section setinggi 3.800px, rasio tingginya 3.800 dibagi 1.440, sekitar 2,64. Rasio lebarnya jauh di bawah itu di layar desktop yang umum: 1.440 dibagi 1.920 sama dengan 0,75, dan 2.560 dibagi 1.920 baru 1,33. Jadi yang menang selalu rasio tinggi, dan gambarnya tergambar 2,6 kali di semua lebar desktop. Di layar Retina dengan DPR 2, angka itu dikali dua: 2,64 kali 2 sama dengan 5,28, sehingga satu piksel berkas melebar menjadi sekitar 5,3 piksel perangkat di tiap sumbu. Di ponsel, pembesarannya terukur 13 kali.
Ketajaman bukan satu-satunya korban. Gambarnya diregangkan ke seluruh tinggi section, sementara layar hanya pernah menampilkan satu irisan dari section itu, jadi yang terlihat selalu sepotong dari komposisinya. Komposisi desainer tidak pernah tampil utuh.
Ground yang di-pin setinggi satu layar
Jalan pintas yang biasa untuk masalah ini adalah background-attachment: fixed. Background yang fixed diposisikan terhadap viewport, sehingga cover menghitung dari layar, bukan dari section. Saya sengaja tidak memakainya, karena iOS mengabaikannya, dan ponsel akan tetap di pembesaran 13 kali. Padahal justru di ponsel angkanya paling parah.
Perbaikan yang mempertahankan desain adalah ground yang di-pin: wrapper absolut yang membentang di seluruh section, dengan anak position: sticky; top: 0; height: 100lvh yang membawa gambar dengan cover. Kelas pin itu dipasang di tiga section yang tinggi tadi, dan setiap section yang di-pin mendapat satu elemen kosong tambahan:
<!-- Disederhanakan, nama kelas disamarkan -->
<section class="ground ground--pinned" style="--ground: url(...)">
<span class="ground__pin" aria-hidden="true"></span>
<!-- isi section -->
</section>CSS-nya, dengan penyamaran nama yang sama:
.ground--pinned {
background-image: none;
}
.ground--pinned > .ground__pin {
display: block;
position: absolute;
inset: 0;
pointer-events: none;
}
.ground--pinned > .ground__pin::before {
content: "";
position: sticky;
top: 0;
display: block;
height: 100vh;
height: 100lvh;
background-image: var(--ground);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}Section-nya berhenti menggambar background sendiri. Wrapper-nya mengisi section lewat inset: 0, berpatokan pada position: relative dari aturan dasar tadi, dan pointer-events: none membuatnya tidak menghalangi klik. Gambarnya dibawa oleh ::before milik wrapper, yang tingginya satu layar dan sticky di top: 0. Elemen sticky tidak bisa keluar dari kotak induknya, dan induknya di sini setinggi section, jadi komentar di kodenya merangkum perilakunya dalam satu kalimat: lapisan itu mulai rata dengan atas section dan lepas di kakinya. Karena kotak yang diisi cover kini seukuran layar, ground selalu tampil di skala layar dan tertahan di tempatnya selama konten bergulir.
::before bisa membaca var(--ground) karena custom property diwariskan dari section ke turunannya, termasuk pseudo-elemen. Gambarnya memang sengaja dikirim sebagai custom property inline per section, bukan ditulis di stylesheet. Hanya dengan cara itu URL berversi bisa sampai ke stylesheet, sedangkan path yang ditulis langsung di CSS akan tersaji basi pada hari klien mengirim tekstur yang sudah dikoreksi.
Dua deklarasi height itu juga disengaja. 100vh menjadi cadangan untuk peramban yang belum mengenal lvh, dan peramban yang mengenalnya memakai deklarasi kedua. Alasan memilih lvh tercatat di komentar kodenya: satuan itu mewakili viewport tertinggi, sehingga bilah alamat yang mengecil tidak pernah membuka celah di bawah lapisan.
Kalau pola ini ditiru di tempat lain, ada satu aturan umum CSS yang perlu diingat. Elemen ber-position: absolute dilukis di atas blok biasa yang tidak ter-posisi, jadi isi section perlu ter-posisi juga, misalnya dengan position: relative, supaya tetap berada di atas lapisan gambar.
Dua section lain mendapat varian kelas yang hanya berlaku di layar sempit. Di desktop, tinggi keduanya kira-kira satu layar, dan di sana cover sudah benar. Begitu kolom-kolomnya bertumpuk, tingginya menjadi dua sampai tiga layar, dan di situ cover kembali salah. Karena itu ground-nya hanya di-pin di bawah max-width: 59.9375em, atau sekitar 959px kalau ukuran font bawaan peramban 16px.
Ground yang tingginya pasti satu layar juga membuat bagian gambar yang terlihat bisa dihitung. Di layar yang lebih ramping daripada rasio 4:3 gambarnya, cover mengikuti tinggi, dan jumlah kolom gambar yang tampil sama dengan 1.440 dikali rasio aspek layar. Di layar beraspek 0,864 atau kurang, itu berarti paling banyak 1.440 × 0,864 = 1.244 kolom. Sehari kemudian, pada 25 September, salah satu background bergrain mendapat pasangan potret berupa potongan tengah 1.246 dari 1.920 kolomnya, dipotong dari master desainer. Potongan itu sedikit lebih lebar daripada 1.244 kolom yang bisa tampil, jadi dia menggambar piksel yang sama dengan 549 KB lebih ringan di layar pertama halamannya.
WebP lossy memakan grain
Sebab kedua ada di encoder. Tekstur bernomor kiriman klien dipasang pada 23 September sebagai WebP q84, satu per section. Pada ground bergrain, WebP lossy tidak bisa mempertahankan grain. Diukur terhadap aslinya, WebP mentok di sekitar 33 sampai 34 dB, bahkan di q95. Satu background bergrain hanya 32,99 dB terhadap aslinya sebelum perbaikan, dengan grain yang terhaluskan.
Angka desibel itu PSNR, ukuran seberapa jauh piksel hasil encode menyimpang dari piksel aslinya. Skalanya logaritmik, dan makin tinggi angkanya, makin dekat hasilnya ke berkas asli.
Catatan saya memberi penjelasannya dalam tanda kurung: chroma subsampling melumerkan grain, dan itulah persisnya yang terbaca sebagai blur. WebP lossy memang selalu menyimpan warna di 4:2:0, setengah resolusi di tiap sumbu. Tapi penjelasan itu harus dibaca bersama hasil berikutnya. AVIF 4:4:4 q88, yang menyimpan warna di resolusi penuh, tidak lebih baik, dan tidak satu pun berkas AVIF 4:4:4 yang lolos 45 dB. Jadi membuang subsampling saja tidak cukup untuk menyelamatkan grain di pengujian itu.
Yang menahan grain adalah mode near-lossless. cwebp -near_lossless 60 menahan 46 sampai 47 dB pada kira-kira 60 sampai 80 persen ukuran JPEG-nya. Opsi ini memakai encoder lossless WebP, didahului prapemrosesan yang sedikit menggeser nilai piksel supaya lebih mudah dikompres. Nilai 100 berarti tanpa prapemrosesan, dan makin kecil angkanya, makin agresif penyesuaiannya. Bentuk minimalnya:
# Contoh minimal, nama berkas netral
cwebp -near_lossless 60 asli.jpg -o hasil.webpGradien halus lain cerita. Lossy q92 aman untuknya, 48 sampai 50 dB dengan ukuran puluhan KB. Karena itu aturannya bukan satu format untuk semua berkas, melainkan pilihan per berkas berdasarkan PSNR terhadap aslinya, dengan target minimal 45 dB.
Setiap background di-encode ulang dari berkas desainer, dan semuanya kini 45 dB atau lebih terhadap aslinya. Tiga berkas memakai near-lossless 60, termasuk dua background bergrain, masing-masing 1,5 sampai 2,2MB. Gradien memakai lossy q92 sampai q97, dengan ukuran 14 sampai 298KB.
Harga grain itu terbaca di diff commit-nya. Background bergrain yang tadi terukur 32,99 dB naik dari 446.604 byte menjadi 1.574.518 byte, kira-kira 3,5 kali. Background bergrain kedua naik dari 858.998 byte menjadi 1.951.808 byte, kira-kira 2,3 kali, tapi kenaikan ini tidak murni efek encoder, karena di commit yang sama berkasnya juga diganti dengan versi baru dari desainer yang membawa grain.
Pilihan per berkas kadang berarti tidak mengonversi sama sekali. Untuk foto pembuka 9:16 yang dikirim klien sehari kemudian, JPEG kiriman klien dipakai apa adanya, karena satu-satunya WebP yang mempertahankan grain-nya, yaitu yang near-lossless, ukurannya dua kali lipat.
Berat halaman naik, dan itu disengaja
Berkas yang setia pada aslinya juga lebih berat. Kenaikan itu disengaja, karena klien meminta hasil setajam berkas aslinya. Total ground di beranda menjadi sekitar 4,3MB, dua di antaranya 2,2MB dan 1,9MB, dari sebelumnya sekitar 1MB.
Berat itu punya efek samping di koneksi lambat. Background CSS mulai diunduh begitu section-nya diberi gaya, terlihat di layar atau tidak. Saat koneksi di-throttle ke kecepatan ponsel lambat, satu background di bawah lipatan tiba lebih dulu daripada foto hero dan menahannya sekitar sepuluh detik.
Perbaikannya adalah menahan ground. Sebuah skrip menandai dokumen dengan kelas penahan, lalu melepas tiap section dengan kelas kedua dalam dua tahap. Ground yang benar-benar ada di layar dilepas seketika. "Benar-benar" di sini berarti tumpang tindih minimal satu piksel, karena section di bawah hero menempel tepat di garis lipatan, dan IntersectionObserver menghitung tepi yang sekadar bersentuhan sebagai berpotongan. Jangkauan ke depan sejauh satu layar baru dimulai setelah event load milik window, ketika foto hero sudah masuk.
Aturan penahannya menghapus background di section maupun di ::before milik pin selama kelas pelepasnya belum terpasang:
/* Nama kelas disamarkan */
html.ground-lazy .ground:not(.is-ground-in),
html.ground-lazy .ground:not(.is-ground-in) > .ground__pin::before {
background-image: none;
}Selama ditahan, warna dasar section yang tampil. Hasilnya terukur di koneksi 1,6 Mbps: foto hero selesai dalam 15,4 detik, dari sebelumnya 25,2 detik. Selisihnya 9,8 detik, sejalan dengan sekitar sepuluh detik yang tadi direbut background di bawah lipatan. Penahannya juga gagal dengan aman. Kalau observer-nya tidak tersedia, semua ground dilepas, dan tanpa skrip tidak ada yang ditahan.
Ekspor yang lebih besar belum tentu menolong
Sebelum menjanjikan ketajaman ke klien, ada satu hal lagi yang perlu diperiksa, yaitu apa sebenarnya isi berkas sumbernya. Membuka PSD papan kerja desainer, yang besarnya 188MB, memperlihatkan bahwa beberapa ground dirakit dari potongan kecil. Isian warna vektor dan logo 3600px memang bisa diekspor lebih besar. Tapi satu tekstur di dalamnya adalah gambar AI 1122x1402 yang ditarik menjadi 1920x2399, sebuah foto di dua background lain hanya 960x1200, dan grunge di salah satu background bergrain hanya 1920.
Hitungannya sederhana. 1.920 dibagi 1.122 sekitar 1,71, dan 2.399 dibagi 1.402 juga sekitar 1,71, jadi tekstur itu sudah diperbesar sekitar 1,71 kali di dalam PSD, sebelum CSS menyentuhnya. Di bagian seperti itu, tidak ada ekspor yang bisa menambah detail.
Batas yang sama berlaku pada ukuran berkasnya. Versi lama salah satu background dulu berukuran 2544x1904, tapi versi baru yang cocok dengan mockup hanya 1920, sehingga di layar 2560 hasilnya sedikit lebih lembut daripada versi lama. Di layar 2560 dan Retina, ekspor 3840px dari desainer hanya akan menolong background gradien dan lettering.
Penjelasan untuk klien, dan yang belum terjawab
Ground yang di-pin dan encode ulangnya masuk ke repo pada 24 September 2026 pukul 12.33.19. Sebelum commit, perbaikannya dibangun dan diverifikasi lokal, lalu produksi diunggah ulang. Karena tree akhirnya sama dengan yang terverifikasi di produksi, isi produksi sama dengan repo.
Sore harinya saya mengirim pesan update ke klien, dibuka dengan kabar bahwa versi baru sudah tayang. Pesan itu mengakui bahwa klien benar dan penyebabnya ada di pihak kami, bukan di berkas desainer. Masalah pertama saya jelaskan tanpa istilah CSS: di halaman yang panjang, tiap background diregangkan ke seluruh tinggi halaman, sekitar dua setengah kali ukuran aslinya dan lebih lagi di ponsel, dan kini tampil seukuran layar serta diam di tempat selama konten bergulir di atasnya. Masalah kedua, kompresi yang dipakai menghaluskan grain di background bertekstur, dan semua background kini di-encode ulang dari berkas asli dengan kualitas mendekati asli.
Pesan yang sama menyebut bahwa berkas background-nya selebar 1920px dan menawarkan ekspor 3840px dari desainer. Ekspor itu paling menolong background gradien dan logo, sedangkan potongan yang memang kecil sejak awal hanya terbantu kalau desainer punya original beresolusi lebih tinggi. Tawaran itu tercatat tanpa jawaban.
Yang terverifikasi dengan angka adalah encode-nya: setiap background 45 dB atau lebih terhadap aslinya. Untuk ground yang di-pin, catatan saya tidak menyimpan angka skala sesudah perbaikan. Yang tertulis hanya hasil yang dimaksud, yaitu ground yang selalu tampil di skala layar. Konfirmasi dari klien bahwa background-nya kini terlihat tajam juga belum ada.
Keluhan "blur" itu ternyata berisi tiga hal yang masing-masing bisa diukur: skala per section, PSNR per berkas, dan resolusi potongan di dalam PSD. Dua yang pertama ada di pihak saya dan bisa saya perbaiki. Yang ketiga hanya bisa dijawab oleh berkas yang lebih besar dari desainer, kalau berkas itu memang ada.