Showcase: Portfolio 3D — Creative Web

Showcase: Portfolio 3D Saatnya menggabungkan semua yang sudah dipelajari untuk membuat sebuah mini portfolio 3D — halaman web interaktif yang menampilkan…

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))

Batas GPU di HP

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:

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

Dampak ke Lighthouse

Skor performa Lighthouse 13 ditimbang dari TBT 30%, LCP 25%, CLS 25%, FCP 10% dan Speed Index 10%. Untuk halaman 3D:

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

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.

Yang akan kamu pelajari