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.
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. 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.
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 một hành tinh; bật HUD debug để xem renderer.info.
Kiến trúc render-loop có thể mở rộng
Mẫu đồ chơi — một hàm animate() khổng lồ — sụp khi vượt vài trăm dòng. 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.timer = new THREE.Timer();
this.timer.connect(document);
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((t) => this.#frame(t)); }
stop() { this.renderer.setAnimationLoop(null); } // ← crucial for SPAs
#frame(timestamp) {
this.timer.update(timestamp);
const dt = this.timer.getDelta();
for (const o of this.updatables) o.tick(dt);
this.composer.render();
}
}
Hai chi tiết senior ẩn ở đây:
renderer.setAnimationLoop()thay chorequestAnimationFrametrần để tương thích tốt với renderer và WebXR. Nó không phải lifecycle policy: app vẫn cần chủ động suspend/resume theo visibility, viewport và route.- Một danh sách
updatablesđể 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ứ.
Màn hình nạp — quản lý sự chờ, đừng giấu nó
Asset thật mất thời gian, và một canvas đen đứng hình bị hiểu là “hỏng.” 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);
Nối mọi loader vào một manager, hiện trạng thái từ onProgress, và hé lộ scene khi onLoad. Tỉ lệ loaded / total là số item, không phải số byte; muốn byte progress cần đi qua fetch/response stream hoặc manifest có kích thước. Demo tổng kết chỉ mô phỏng trạng thái chờ, không được dùng để khẳng định pipeline asset thật đã được đo.
Responsive — ResizeObserver, không phải window ‘resize’
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. 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);
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ó.
Tích hợp vào framework
Three.js là mệnh lệnh; React/Vue/Svelte là khai báo. 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} />;
}
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. 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.
Công cụ debug
Không thể tinh chỉnh thứ bạn không thấy. Senior nối những thứ này từ sớm:
renderer.infotrên màn hình — draw call, tam giác, geometry, texture.- 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. - Helper khi phát triển — gỡ khi production.
window.scene = scenetrong dev để chọc graph từ console.
Checklist triển khai
Trước khi ship một scene Three.js:
- Nén asset — Draco cho mesh, KTX2/Basis cho texture,
.glbhơn.gltf. - Giới hạn pixel ratio ở 2 và test trên điện thoại tầm trung thật.
- Tôn trọng
prefers-reduced-motion— chậm hoặc dừng animation nhàn rỗi. - Xử lý WebGL lỗi — kiểm tra và hiện ảnh dự phòng, đừng để canvas trắng.
- Lazy-load bundle 3D nặng để không chặn lần vẽ đầu.
- Dispose khi unmount và xác nhận
renderer.info.memoryphẳng qua các lần điều hướng. - Tone mapping + quản lý màu bật, để production trông như dev.
Lời cảnh báo của sư phụ
- Scene trắng sau khi đổi route? Bạn tạo lại
Worldtrong render, hoặc chưa dispose cái cũ. - Canvas không resize trong panel? Dùng
ResizeObservertrên canvas. - Animation chạy hoài ngoài viewport? Dùng
visibilitychange/IntersectionObserverđểstop()vàstart()tường minh;setAnimationLoopkhông thay policy đó. - Tốt trên desktop, chảy trên mobile? Pixel ratio và asset chưa nén.
Checkpoint — từ feature sang production engineering
Bạn đã đi từ canvas trắng tới một scene tương tác có loading state, resize, lifecycle và debug HUD. Đây là checkpoint, chưa phải đích: các phần sau đi vào shader, physics, asset pipeline, animation nhân vật, toán không gian, CPU/GPU profiling, ownership, testing và WebGPU/TSL.
Từ đây tiêu chuẩn đổi: không chỉ làm cho scene chạy, mà phải chứng minh nó giữ budget, recover được và có contract để cả team cùng phát triển.