jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Part 10 — Capstone: Production & Deployment

Tie it all together into a shippable mini-project: loading screens, ResizeObserver responsiveness, a render-loop architecture that scales, framework integration, debug tooling, and a real deployment checklist — with a full capstone demo.

You’ve built the pieces — scenes, materials, lights, models, animation, post-processing, performance, interaction. {Bạn đã dựng các mảnh — scene, vật liệu, đèn, model, animation, hậu kỳ, hiệu năng, tương tác.} A senior’s last job is assembly: turning those pieces into something that loads gracefully, resizes correctly, survives a framework, and ships. {Việc cuối của một senior là lắp ráp: biến các mảnh đó thành thứ nạp mượt, resize đúng, sống sót trong framework, và ship được.}

This capstone scene uses everything from the series at once — a loading screen, a bloom-lit sun, an instanced asteroid belt, click-to-select planets, responsive resize, and a live debug HUD. {Scene tổng kết này dùng mọi thứ trong series cùng lúc — màn hình nạp, mặt trời rực bloom, vành đai thiên thạch instanced, hành tinh click-để-chọn, resize responsive, và HUD debug trực tiếp.} Click a planet; toggle the debug HUD to watch renderer.info. {Click một hành tinh; bật HUD debug để xem renderer.info.}

Open the full demo {Mở demo đầy đủ}: /tools/threejs-capstone-demo/.

A render-loop architecture that scales {Kiến trúc render-loop có thể mở rộng}

The toy pattern — one giant animate() function — falls apart past a few hundred lines. {Mẫu đồ chơi — một hàm animate() khổng lồ — sụp khi vượt vài trăm dòng.} A maintainable Three.js app separates concerns into a small set of objects: {Một app Three.js dễ bảo trì tách mối quan tâm thành một bộ object nhỏ:}

class World {
  constructor(canvas) {
    this.canvas = canvas;
    this.scene = new THREE.Scene();
    this.camera = createCamera();
    this.renderer = createRenderer(canvas);
    this.composer = createComposer(this.renderer, this.scene, this.camera);
    this.clock = new THREE.Clock();
    this.updatables = [];          // anything with a .tick(dt) method
    this.#bindResize();
  }
  add(obj) { this.scene.add(obj); if (obj.tick) this.updatables.push(obj); }
  start() { this.renderer.setAnimationLoop(() => this.#frame()); }
  stop()  { this.renderer.setAnimationLoop(null); }   // ← crucial for SPAs
  #frame() {
    const dt = this.clock.getDelta();
    for (const o of this.updatables) o.tick(dt);
    this.composer.render();
  }
}

Two senior details hide here: {Hai chi tiết senior ẩn ở đây:}

  • renderer.setAnimationLoop() instead of bare requestAnimationFrame — it pauses automatically when the tab is hidden and is required for WebXR. {renderer.setAnimationLoop() thay cho requestAnimationFrame trần — nó tự dừng khi tab ẩn và bắt buộc cho WebXR.}
  • An updatables list so each object owns its own per-frame logic via a tick(dt) method — no central function that knows about everything. {Một danh sách updatables để mỗi object tự sở hữu logic mỗi frame qua tick(dt) — không có hàm trung tâm biết hết mọi thứ.}

Loading screens — manage the wait, don’t hide it {Màn hình nạp — quản lý sự chờ, đừng giấu nó}

Real assets take time, and a frozen black canvas reads as “broken.” {Asset thật mất thời gian, và một canvas đen đứng hình bị hiểu là “hỏng.”} Three.js gives you a global LoadingManager that fires progress and completion callbacks across all loaders: {Three.js cho bạn một LoadingManager toàn cục bắn callback tiến trình và hoàn tất xuyên mọi loader:}

const manager = new THREE.LoadingManager();
manager.onProgress = (url, loaded, total) => setBar(loaded / total);
manager.onLoad = () => hideLoadingScreen();   // all assets ready → reveal

const gltfLoader = new GLTFLoader(manager);    // pass the manager to each loader
const texLoader  = new THREE.TextureLoader(manager);

Wire every loader to one manager, show a progress bar from onProgress, and reveal the scene on onLoad. {Nối mọi loader vào một manager, hiện thanh tiến trình từ onProgress, và hé lộ scene khi onLoad.} The capstone demo simulates this with a timed bar — the UX is identical to a real asset load. {Demo tổng kết mô phỏng bằng thanh hẹn giờ — UX giống hệt một lần nạp asset thật.}

Responsiveness — ResizeObserver, not window ‘resize’ {Responsive — ResizeObserver, không phải window ‘resize’}

The window resize event misses the common cases: a canvas inside a flex/grid panel, a sidebar collapsing, an embedded iframe. {Sự kiện resize của window bỏ lỡ các trường hợp phổ biến: canvas trong panel flex/grid, sidebar thu lại, iframe nhúng.} Observe the canvas itself: {Hãy quan sát chính canvas:}

const ro = new ResizeObserver(() => {
  const { clientWidth: w, clientHeight: h } = canvas;
  if (!w || !h) return;                       // skip while hidden (0×0)
  renderer.setSize(w, h, false);              // false = don't touch CSS size
  composer.setSize(w, h);                     // keep the composer in sync
  camera.aspect = w / h; camera.updateProjectionMatrix();
});
ro.observe(canvas);

The false flag on setSize is important when the canvas is sized by CSS — you want WebGL to match the element, not overwrite its style. {Cờ false trên setSize quan trọng khi canvas được CSS định kích thước — bạn muốn WebGL khớp với phần tử, không ghi đè style của nó.}

Integrating into a framework {Tích hợp vào framework}

Three.js is imperative; React/Vue/Svelte are declarative. {Three.js là mệnh lệnh; React/Vue/Svelte là khai báo.} The bridge rule is simple: create the World once, never inside render, and always tear it down on unmount. {Quy tắc cầu nối đơn giản: tạo World một lần, không bao giờ trong render, và luôn dọn dẹp khi unmount.}

function Scene() {
  const ref = useRef(null);
  useEffect(() => {
    const world = new World(ref.current);
    world.start();
    return () => { world.stop(); world.dispose(); }; // stop loop + free GPU memory
  }, []);                                            // empty deps → run once
  return <canvas ref={ref} />;
}

Forget the cleanup and an SPA leaks a whole WebGL context (and its VRAM) on every navigation. {Quên dọn dẹp thì SPA rò rỉ cả một WebGL context (và VRAM của nó) mỗi lần điều hướng.} If you write a lot of React, react-three-fiber does this bridging for you — but knowing what it automates is exactly what separates senior from cargo-cult. {Nếu bạn viết nhiều React, react-three-fiber làm cầu nối này cho bạn — nhưng biết nó tự động hoá cái gì chính là thứ phân biệt senior với làm theo kiểu mê tín.}

Debug tooling {Công cụ debug}

You can’t tune what you can’t see. {Không thể tinh chỉnh thứ bạn không thấy.} A senior wires these in early: {Senior nối những thứ này từ sớm:}

  • renderer.info on screen — draw calls, triangles, geometries, textures. The capstone’s debug HUD shows exactly this. {renderer.info trên màn hình — draw call, tam giác, geometry, texture.}
  • A GUI panel (lil-gui) bound to live values so you tune without editing code. {Một panel GUI (lil-gui) gắn với giá trị trực tiếp để tinh chỉnh không cần sửa code.}
  • Helpers during development — AxesHelper, GridHelper, CameraHelper, light helpers — removed for production. {Helper khi phát triển — gỡ khi production.}
  • window.scene = scene in dev so you can poke the graph from the console. {window.scene = scene trong dev để chọc graph từ console.}

The deployment checklist {Checklist triển khai}

Before you ship a Three.js scene: {Trước khi ship một scene Three.js:}

  • Compress assets — Draco for meshes, KTX2/Basis for textures, .glb over .gltf. {Nén asset — Draco cho mesh, KTX2/Basis cho texture, .glb hơn .gltf.}
  • Cap pixel ratio at 2 and test on a real mid-range phone, not just your laptop. {Giới hạn pixel ratio ở 2 và test trên điện thoại tầm trung thật.}
  • Respect prefers-reduced-motion — slow or stop idle animation for users who ask. {Tôn trọng prefers-reduced-motion — chậm hoặc dừng animation nhàn rỗi.}
  • Handle WebGL failureWebGL.isWebGLAvailable() and show a fallback image, never a blank canvas. {Xử lý WebGL lỗi — kiểm tra và hiện ảnh dự phòng, đừng để canvas trắng.}
  • Lazy-load the heavy 3D bundle so it doesn’t block first paint. {Lazy-load bundle 3D nặng để không chặn lần vẽ đầu.}
  • Dispose on unmount and confirm renderer.info.memory is flat across navigations. {Dispose khi unmount và xác nhận renderer.info.memory phẳng qua các lần điều hướng.}
  • Tone mapping + color management on, so it looks the same in production as in dev. {Tone mapping + quản lý màu bật, để production trông như dev.}

The master’s warnings {Lời cảnh báo của sư phụ}

  • Scene blank after a route change? You re-created the World in render, or never disposed the old one. {Scene trắng sau khi đổi route? Bạn tạo lại World trong render, hoặc chưa dispose cái cũ.}
  • Canvas not resizing in a panel? Use ResizeObserver on the canvas, not window resize. {Canvas không resize trong panel? Dùng ResizeObserver trên canvas.}
  • Animation keeps running on a hidden tab? Use renderer.setAnimationLoop, which pauses automatically. {Animation chạy hoài trên tab ẩn? Dùng renderer.setAnimationLoop.}
  • Great on desktop, melts on mobile? Pixel ratio and uncompressed assets — fix those first. {Tốt trên desktop, chảy trên mobile? Pixel ratio và asset chưa nén.}

Where to go from here {Đi đâu tiếp theo}

You’ve gone from a blank canvas to a production-grade interactive scene. {Bạn đã đi từ canvas trắng tới một scene tương tác mức production.} The senior frontier from here: custom GLSL shaders (ShaderMaterial, the GPU language itself), physics (Rapier / cannon-es), WebXR for VR/AR, and react-three-fiber for declarative scenes at scale. {Biên giới senior từ đây: shader GLSL tuỳ biến, vật lý (Rapier / cannon-es), WebXR cho VR/AR, và react-three-fiber cho scene khai báo ở quy mô lớn.} But the mental model you now hold — scene graph, the render pipeline, the GPU cost of everything — carries into all of them. {Nhưng mô hình tư duy bạn đang nắm — scene graph, pipeline render, chi phí GPU của mọi thứ — theo bạn vào tất cả.}

That’s the series. {Đó là cả series.} From gà mờ to senior — you now know not just how to make it work, but why it works and what it costs. {Từ gà mờ tới senior — giờ bạn biết không chỉ làm sao cho nó chạy, mà vì sao nó chạy và nó tốn gì.} Go build something. {Đi dựng một thứ gì đó đi.}