D
P
0
← Semua artikel Read in English

JavaScript, DOM & Animasi Browser

Hero Canvas Pecah di HP Asli? Pemilih Tier-nya Membaca `clientWidth`, Bukan Piksel Perangkat

· · 5 menit baca
Hero Canvas Pecah di HP Asli? Pemilih Tier-nya Membaca `clientWidth`, Bukan Piksel Perangkat

Hero di halaman itu bukan elemen <video>. Yang tampil sebenarnya 193 frame WebP yang digambar satu per satu ke <canvas>, dan frame mana yang muncul ditentukan oleh posisi scroll. Di HP asli hasilnya pecah. Bukan pecah samar yang baru kelihatan kalau dicari, tapi pecah yang langsung terbaca sebagai gambar kecil yang dipaksa jadi besar, dan waktu diukur ternyata HP potret memang dilayani frame tier 720 yang direntang sekitar empat kali lipat.

Frame-nya sendiri tersedia dalam tiga ukuran, tier 720, tier 1280, dan tier 1920. Jadi tier yang lebih besar ada di server, cuma tidak pernah dipilih. Fungsi yang memilihnya membaca satu angka saja, yaitu clientWidth.

// Inti versi lama: satu sumbu, satuan piksel CSS.
const tier = tiers.find((t) => t.w >= canvas.clientWidth) ?? tiers.at(-1);

Satu angka itu kehilangan dua hal sekaligus

clientWidth dilaporkan dalam piksel CSS, dan HP potret punya lebar CSS yang kecil. Ada dua hal yang tidak pernah masuk ke angka itu. Pertama, device pixel ratio-nya 2 sampai 3, jadi setiap piksel CSS diisi dua sampai tiga piksel perangkat. Kedua, canvas-nya cover-fill viewport, artinya frame harus menutupi sumbu tinggi juga, dan di HP potret tinggi justru sisi panjangnya. Digabung, kebutuhan piksel perangkat di kedua sumbu jauh lebih tinggi dari yang disiratkan clientWidth.

Sisi mana yang sebenarnya mengikat bisa dilihat dari geometri framenya. Frame yang baru diturunkan dari sebuah klip video berukuran 1764x1176. Angka 1764 dibagi 1176 hasilnya 1,5 persis, jadi rasionya 3:2. Kalau rasio itu dipertahankan waktu diturunkan jadi tiga tier, tinggi tiap tier adalah lebarnya dibagi 1,5: tier 720 setinggi 480, tier 1280 setinggi sekitar 853, dan tier 1920 setinggi 1280.

Tinggi itulah yang biasanya kalah lebih dulu di HP potret, dan cek yang hilang bukan cuma pengali DPR-nya, tapi juga sisi kedua yang tidak pernah ditanyakan. Sebuah tier bisa saja cukup lebar dan tetap terlalu pendek, dan clientWidth tidak punya cara untuk menceritakan itu.

Aturan yang menggantikannya

Aturan penggantinya menghitung kebutuhan lebih dulu, baru mencari tier yang memenuhinya. neededW adalah lebar viewport dikali min(DPR, 2), neededH adalah tinggi viewport dikali min(DPR, 2), lalu yang dipilih adalah tier terkecil yang lebarnya lebih besar atau sama dengan neededW DAN tingginya lebih besar atau sama dengan neededH. Kalau tidak ada yang memenuhi keduanya, jatuh ke tier 1920.

function pickTier(viewportW, viewportH, tiers) {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const neededW = viewportW * dpr;
  const neededH = viewportH * dpr;
 
  // tiers sudah urut dari yang terkecil
  const cocok = tiers.find((t) => t.w >= neededW && t.h >= neededH);
  return cocok ?? tiers.at(-1); // tier 1920
}

Dua keputusan di dalamnya layak dieja. Yang pertama, DPR-nya dibatasi 2. Karena rentang yang dihadapi 2 sampai 3, perangkat DPR 3 diperlakukan seolah DPR-nya 2, dan kebutuhan yang dihitung untuk perangkat itu jadi dua pertiga dari kebutuhan sebenarnya. Itu memang kompromi yang dipilih, bukan kelalaian yang tersisa.

Yang kedua, fallback-nya bukan tier terkecil melainkan yang terbesar. Kalau tidak ada satu pun tier yang cukup di kedua sumbu, yang paling masuk akal disajikan adalah yang paling dekat dengan cukup, bukan yang paling murah.

Yang terjadi setelah aturan itu dipasang

Efeknya blak-blakan. HP asli dengan DPR 2 ke atas sekarang mengambil tier 1920. Pemeriksaan di situs hidup menunjukkan seluruh pengambilan frame adalah tier 1920, di desktop maupun di mobile DPR-2, dan preloader-nya naik dari 0 ke 100 dengan mulus.

Jadi tangga tiga anak itu praktis runtuh jadi anak tangga teratas saja untuk apa pun yang layarnya kelas retina. Saya tidak membaca itu sebagai desain responsif yang gagal. Itu jawaban jujur atas pertanyaan yang benar. Begitu pertanyaannya berubah dari "berapa lebar kotak ini" jadi "berapa piksel perangkat yang harus saya isi di dua sumbu", nyaris semua perangkat modern memang meminta tier teratas, dan versi lama cuma tidak pernah menanyakannya.

Kenapa mengirim tier 1920 ke HP masih masuk akal

Set frame yang baru isinya 193 frame dikali tiga tier, WebP q80-82 dengan penskalaan lanczos, totalnya 38MB. Set lama yang digantikannya berisi 386 frame dan totalnya 386MB.

Hitungannya bisa diulang sendiri. 193 dikali 3 sama dengan 579 berkas, dan itu persis jumlah berkas frame yang ikut masuk waktu perubahannya di-commit. 38MB dibagi 579 berkas keluar di sekitar 0,066MB rata-rata per berkas. Di set lama, 386MB dibagi 386 frame keluar tepat 1MB per frame. Lalu 386 dibagi 38 sama dengan sekitar 10,2, jadi seluruh set baru itu sekitar sepersepuluh set lama, dengan jumlah frame tepat setengahnya karena 386 dibagi 193 sama dengan 2.

Dengan berkas sekecil itu, menyajikan tier teratas ke HP berhenti jadi keputusan yang perlu ditawar. Yang membuat perbaikan tier-nya murah bukan kepintaran aturannya, tapi kenyataan bahwa aset yang dipilihnya sudah jauh lebih ringan lebih dulu.

Verifikasi dan sisa yang sengaja ditunda

Perubahannya saya periksa dulu di instans WordPress Studio lokal. Commit yang membawanya menambahkan 579 frame baru dan menyentuh 65 berkas.

Satu hal saya tahan sampai belakangan. Commit yang sama itu sekaligus menghapus folder set lama dari repo, tapi salinannya di server masih duduk di sana dalam keadaan tidak dirujuk siapa pun, dan saya catat sebagai kandidat penghapusan setelah verifikasi di situs hidup, bukan sebelumnya. Setelah pemeriksaan live lolos, folder itu saya buang dari server, yang sekarang membalas 404 untuk path lamanya. Urutan itu sengaja untuk sisi server: salinan lama baru boleh hilang dari sana setelah aset baru terbukti dilayani dengan benar, bukan waktu dia terlihat sudah tidak dipakai.

Yang saya bawa dari kasus ini

clientWidth menjawab pertanyaan "berapa lebar kotak ini dalam piksel CSS". Yang ditanyakan sebuah canvas yang cover-fill adalah "berapa piksel perangkat yang harus saya isi, di dua sumbu". Keduanya kebetulan menghasilkan angka yang mirip di monitor desktop DPR 1, dan mulai berbeda jauh persis di kelas perangkat yang paling banyak dipakai pengunjung.

Sejak itu, tiap kali saya menulis pemilih ukuran gambar, saya paksa diri menuliskan kebutuhannya dalam piksel perangkat di kedua sumbu lebih dulu, baru mencari aset yang memenuhinya. Kalau penjaganya cuma membandingkan satu angka dengan satu angka, kemungkinan besar dia sedang menjawab pertanyaan yang lebih kecil dari yang sebenarnya diajukan.