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 barerequestAnimationFrame— it pauses automatically when the tab is hidden and is required for WebXR. {renderer.setAnimationLoop()thay chorequestAnimationFrametrần — nó tự dừng khi tab ẩn và bắt buộc cho WebXR.}- An
updatableslist so each object owns its own per-frame logic via atick(dt)method — no central function that knows about everything. {Một danh sáchupdatablesđể mỗi object tự sở hữu logic mỗi frame quatick(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.infoon screen — draw calls, triangles, geometries, textures. The capstone’s debug HUD shows exactly this. {renderer.infotrê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 = scenein dev so you can poke the graph from the console. {window.scene = scenetrong 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,
.glbover.gltf. {Nén asset — Draco cho mesh, KTX2/Basis cho texture,.glbhơ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ọngprefers-reduced-motion— chậm hoặc dừng animation nhàn rỗi.} - Handle WebGL failure —
WebGL.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.memoryis flat across navigations. {Dispose khi unmount và xác nhậnrenderer.info.memoryphẳ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
Worldin render, or never disposed the old one. {Scene trắng sau khi đổi route? Bạn tạo lạiWorldtrong render, hoặc chưa dispose cái cũ.} - Canvas not resizing in a panel? Use
ResizeObserveron the canvas, not windowresize. {Canvas không resize trong panel? DùngResizeObservertrê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ùngrenderer.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.}