Halaman 3D yang bagus bukan cuma soal adegan yang keren, tapi soal halaman yang tetap jalan saat 3D-nya lambat, berat, atau gak bisa tampil. Bayangin etalase toko Raka dengan lampu sorot: lampunya membuat mug terlihat memukau, tapi kalau lampunya mati, mug, label harga dan kasirnya tetap ada. Di web, 3D adalah lampu sorot itu: tambahan, bukan syarat.
Lesson terakhir track ini menyatukan semuanya di halaman produk Raka: etalase 3D (mug dan kotak kaos), dengan anggaran performa, pemuatan bertahap, cadangan tanpa WebGL, dan aksesibilitas.
🧪 Tebakan cepat: Raka menaruh
import * as THREE from 'three'langsung di bundle utama halaman produk. Di setelan Lighthouse mobile, kira-kira berapa lama three.js saja diunduh, dan berapa lama main thread tertahan saat menjalankannya? Tebak dulu, jawabannya ada di bagian "Pemuatan".
Anggaran performa
Hitung dulu sebelum menambah apa pun. Untuk etalase Raka (angka dari renderer.info di three.js 0.186):
| Yang dihitung | Etalase Raka | Cara menghitung |
|---|---|---|
| Draw call | 10 | 1 per mesh per grup material: badan mug 3 + gagang 1 + kotak kaos 6 |
| Segitiga | 1.228 | 192 (badan 48 segmen) + 1.024 (gagang) + 12 (kotak) |
| Memori tekstur | 2,28 MB | (942 × 300 + 480 × 300) × 4 byte × 4/3 untuk mipmap |
| Pixel canvas | 307.200 | 640 × 480 di DPR 2, dengan setPixelRatio(Math.min(devicePixelRatio, 2)) |
- Draw call adalah satu perintah gambar dari CPU ke GPU. Ratusan draw call membuat CPU HP kewalahan duluan sebelum GPU-nya sibuk. Kurangi dengan menggabungkan geometry yang materialnya sama (
BufferGeometryUtils.mergeGeometries),InstancedMeshuntuk banyak benda identik (100 mug = 1 draw call), dan satu tekstur gabungan (atlas) supaya kotak kaos cukup 1 material, bukan 6. - Memori tekstur dihitung dari ukuran pixel, bukan ukuran file: PNG sablon mug cuma 14 KB, tapi di GPU jadi 1,5 MB. Satu tekstur 4096 × 4096 sudah sekitar 89 MB.
renderer.info.memory.texturesdi sini menunjukkan 3: dua tekstur kita plus satu tabel 16 × 16 milik MeshStandardMaterial.
Batas GPU di HP
renderer.capabilities.maxTextureSize(dariMAX_TEXTURE_SIZE): WebGL 2 menjamin paling sedikit 2048. Tekstur yang lebih besar dikecilkan three.js dengan peringatanTHREE.WebGLRenderer: Texture has been resized from (...) to (...). Untuk HP, pakai paling besar 2048.- Saat memori GPU habis, browser bisa mencabut konteksnya: event
webglcontextlost. Tangkap, panggilpreventDefault(), dan tampilkan poster lagi. - HP cepat panas: batasi DPR ke 2, hentikan loop saat etalase di luar layar (
IntersectionObserver), dan jangan render kalau gak ada yang berubah.
Pemuatan: GLB, kompresi, placeholder
three.js untuk adegan ini, di-bundle dan di-minify, 538 KB, 134,6 KB setelah gzip (diukur dengan three 0.186.1). Setelan Lighthouse mobile adalah 1,6 Mbps dengan RTT 150 ms dan CPU 4 kali lebih lambat: unduhannya sekitar 0,81 detik, lalu menjalankan three.js plus render pertama jadi satu long task sekitar 130 ms (diukur di Chrome), yang menambah TBT sekitar 80 sampai 110 ms. Itu jawaban tebakannya. Jadi three.js dipisah lewat import() dan baru dimuat saat dibutuhkan:
// index.js: halaman produk, tanpa three.js
import WebGL from 'three/addons/capabilities/WebGL.js'; // cuma pengecek kecil
async function muat3D() {
const { buatEtalase } = await import('./etalase-3d.js'); // chunk terpisah: three.js
await buatEtalase(document.getElementById('toko-3d'));
}
if (WebGL.isWebGL2Available()) muat3D(); // atau saat tombol "Lihat dalam 3D" diklik
Model 3D biasanya datang sebagai glTF, formatnya dalam satu file biner .glb. Etalase Raka diekspor jadi 66,4 KB (38,6 KB geometry, sisanya tekstur PNG). Model hasil scan atau buatan Blender bisa beberapa MB, dan dua cara mengecilkan geometry-nya:
- Draco (ekstensi
KHR_draco_mesh_compression): biasanya paling kecil, tapi butuhDRACOLoaderdan decoder WebAssembly sekitar 75 KB gzip yang jalan di Worker. - Meshopt (
EXT_meshopt_compression):loader.setMeshoptDecoder(MeshoptDecoder), decoder-nya cuma 7,7 KB gzip dan dekodenya lebih cepat.
Untuk model 66 KB, decoder Draco lebih besar daripada yang dihemat; untuk scan 5 MB, kompresi sangat terasa. Keduanya bisa dibuat dengan npx @gltf-transform/cli draco atau meshopt.
const loader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder);
const gltf = await loader.loadAsync('/model/etalase.glb', (e) => {
wadah.dataset.persen = Math.round((e.loaded / e.total) * 100); // "Memuat 42%"
});
scene.add(gltf.scene);
Selama itu pengunjung harus melihat sesuatu: model bisa berukuran besar, dan canvas kosong terlihat seperti halaman rusak. Placeholder terbaik adalah poster gambar produk di kotak yang ukurannya sudah dicadangkan (aspect-ratio: 4 / 3), plus persentase dari onProgress. Saat 3D siap, poster diganti canvas di tempat yang sama: gak ada yang bergeser. Di R3F, bungkus model dengan <Suspense fallback={...}>; useProgress dari drei memberi persentasenya.
Tanpa WebGL: progressive enhancement
Progressive enhancement artinya halaman dasar (gambar, teks, harga, tombol beli) jalan di semua browser, dan 3D ditambahkan hanya kalau bisa. three.js sejak r163 butuh WebGL 2; tanpa itu new THREE.WebGLRenderer() melempar THREE.WebGLRenderer: Error creating WebGL context. Jadi cek dulu:
<a class="lewati" href="#detail">Lewati 3D, langsung ke detail produk</a>
<div id="toko-3d" style="aspect-ratio: 4 / 3; max-width: 320px">
<img id="poster" src="/img/etalase.webp" width="320" height="240"
alt="Mug Sablon Kopi hitam bersablon cangkir kopi, di sebelah kotak Kaos Sablon Kopi">
</div>
<h2 id="detail" tabindex="-1">Detail produk</h2>
Kalau WebGL.isWebGL2Available() false, muat3D gak pernah dipanggil: poster dan harga tetap, dan 134,6 KB three.js gak diunduh sama sekali.
Aksesibilitas
- Alt text untuk canvas:
canvas.setAttribute('role', 'img')danaria-labelyang menjelaskan isinya. Pembaca layar gak bisa melihat isi canvas. - Link "Lewati 3D" di awal bagian itu, menuju judul detail produk (
tabindex="-1"supaya bisa menerima fokus): pengguna keyboard gak perlu melewati canvas dan semua tombolnya. - Gerakan dikurangi:
controls.autoRotate = !matchMedia('(prefers-reduced-motion: reduce)').matches. - Keyboard: tombol biasa untuk memutar kamera dan memilih produk (lesson Three.js Interaktif).
Dampak ke Lighthouse
Skor performa Lighthouse 13 ditimbang dari TBT 30%, LCP 25%, CLS 25%, FCP 10% dan Speed Index 10%. Untuk halaman 3D:
- TBT: long task three.js masuk hitungan kalau dimuat saat halaman dibuka. Muat saat terlihat atau saat diklik.
- LCP:
<canvas>gak pernah jadi kandidat LCP (yang dihitung gambar dan blok teks). Poster<img>kecil atau judul produklah yang jadi LCP, jadi buat ia cepat tampil. - CLS: kotak yang ukurannya dicadangkan dari awal membuat CLS tetap 0 saat 3D muncul.
Three.js biasa atau R3F
Kalau toko Raka sudah dibangun dengan React, React Three Fiber adalah pilihan yang pas: scene ditulis sebagai komponen, state keranjang dan pilihan warna langsung jadi props, dan drei menyediakan helper. Kalau halamannya statis tanpa React, three.js biasa lebih ringan. Di GPU keduanya menggambar hal yang sama; yang menentukan adalah draw call, segitiga, tekstur dan cara memuatnya.
Satu halaman dari dua belas lesson
| Bagian halaman | Dari lesson |
|---|---|
| Sablon Kopi di tekstur dan poster | Canvas API Dasar |
| Uap kopi, delta waktu | Animasi Canvas |
| Pointer, drag | Canvas Interaktif |
| Logo toko yang bergerak | SVG Animation |
| Motif Pola Biji untuk kaos berikutnya | Generative Art |
| Shader, DPR, resize | WebGL Konsep, Three.js Setup |
Mug, kotak, lampu, dispose() |
Three.js Geometri & Material |
Loop, Timer, autoRotate dengan delta |
Three.js Animation |
| Orbit, raycaster, tombol keyboard | Three.js Interaktif |
| Versi React | React Three Fiber |
| Anggaran, pemuatan, cadangan, aksesibilitas | lesson ini |
⚠️ Jebakan yang sering ketemu
- three.js di bundle utama: 134,6 KB gzip dan long task 130 ms di HP, padahal 3D-nya di bawah. Pakai
import(). - Kotak 3D tanpa ukuran dan tanpa poster: layar kosong saat memuat, lalu teks harga terdorong (CLS).
- Tekstur 4096 untuk HP: 89 MB memori GPU per tekstur, atau dikecilkan diam-diam. Pakai paling besar 2048.
- Tanpa cek WebGL: di perangkat tanpa WebGL 2, halaman error atau kosong.
- Canvas tanpa alt dan tanpa link lewati: pembaca layar dan pengguna keyboard tersesat.
TL;DR: 3D adalah tambahan, bukan syarat. Hitung anggarannya: draw call (1 per mesh per grup material, kurangi dengan merge, InstancedMesh, atlas), segitiga, memori tekstur (lebar × tinggi × 4 × 4/3; etalase Raka 10 draw call, 1.228 segitiga, 2,28 MB), DPR paling besar 2, tekstur paling besar 2048 di HP, tangani webglcontextlost. Pisahkan three.js (134,6 KB gzip) dengan import() dan muat saat terlihat atau diklik; model .glb dengan GLTFLoader, kompresi Draco (KHR_draco_mesh_compression) atau Meshopt (EXT_meshopt_compression). Poster <img> dengan alt di kotak aspect-ratio sebagai placeholder dan cadangan saat WebGL.isWebGL2Available() false. Canvas role="img" + aria-label, link "Lewati 3D", prefers-reduced-motion. Lighthouse: TBT 30%, LCP 25% (canvas bukan kandidat), CLS 25%. Sudah React? R3F; belum? three.js biasa.