Three.js from Zero to Senior · Part 8 — Performance & Instancing
Build a first performance model: draw calls, triangles, pixels, instancing, merging, culling, LOD and resource ownership — then measure before choosing an optimization.
Ai cũng làm được scene mười vật thể chạy 60 fps. Senior làm được scene mười nghìn vật thể chạy 60 fps — và kỹ năng phân biệt là hiểu frame thật sự đi đâu.
Demo vẽ cùng hàng nghìn khối theo hai cách. Đổi giữa InstancedMesh và mesh riêng, tăng số lượng, và xem draw call với FPS kể hai câu chuyện hoàn toàn khác.
Draw call — một ngân sách, không phải ngân sách duy nhất
Mỗi mesh/material group thường tạo một lệnh CPU gửi cho GPU: “dùng pipeline và resource này, vẽ geometry kia”. Một nghìn mesh riêng có thể khiến CPU tốn phần lớn frame để duyệt scene, sort và submit. Nhưng scene khác có thể nghẽn vertex, fragment/fill-rate, bandwidth, texture upload, shader compile hoặc JavaScript update. Draw call là một giả thuyết cần đo, không phải đáp án mặc định.
Bạn đọc được số liệu trực tiếp bất cứ lúc nào:
console.log(renderer.info.render.calls); // draw calls this frame
console.log(renderer.info.render.triangles); // triangles this frame
console.log(renderer.info.memory.geometries, renderer.info.memory.textures);
renderer.info là bộ đếm hữu ích, không phải CPU/GPU profiler và không cho VRAM chính xác. Với composer hoặc nhiều render pass, đặt autoReset = false, cộng cả frame rồi reset sau pass cuối; nếu không bạn có thể chỉ thấy pass cuối:
renderer.info.autoReset = false;
renderShadowAndSceneAndPost();
readHud(renderer.info); // counters của toàn frame
renderer.info.reset();
Calls cao làm CPU submission trở thành nghi phạm. Triangles cao gợi ý vertex cost; drawing-buffer pixels × overdraw × shader complexity gợi ý fragment cost. Part 22 biến các tín hiệu này thành quy trình chẩn đoán CPU/GPU đầy đủ.
InstancedMesh — hàng nghìn vật thể, một draw call
Khi cần nhiều bản sao của cùng geometry và material — cây, đám đông, thiên thạch, gạch — InstancedMesh vẽ tất cả trong một lần gọi. Bạn cấp cho nó ma trận mỗi instance (vị trí/xoay/tỉ lệ) và tuỳ chọn màu mỗi instance:
const mesh = new THREE.InstancedMesh(geometry, material, COUNT);
const dummy = new THREE.Object3D(); // a scratch object to build matrices
for (let i = 0; i < COUNT; i++) {
dummy.position.set(x, y, z);
dummy.rotation.y = Math.random() * Math.PI;
dummy.updateMatrix(); // compose into dummy.matrix
mesh.setMatrixAt(i, dummy.matrix); // write the instance transform
mesh.setColorAt(i, color.setHSL(h, 0.7, 0.55));
}
mesh.instanceMatrix.needsUpdate = true; // upload to the GPU
scene.add(mesh);
Nếu bạn animate instance mỗi frame, đặt setUsage(THREE.DynamicDrawUsage) để GPU biết sẽ cập nhật thường xuyên. Đánh đổi: mọi instance dùng chung một material, nên không thể có shader khác nhau — chỉ khác transform và màu (instance).
Trong pass màu của demo, 5.000 khối dùng chung geometry/material có thể thành một draw call; 5.000 mesh riêng tiến gần N calls. Shadow map, depth prepass hay post pipeline có thể vẽ lại và nhân chi phí — đừng biến “một instance mesh” thành lời hứa “một call cho cả frame”.
Nếu transform instance thay đổi, cập nhật attribute và bounding volume dùng cho culling:
mesh.instanceMatrix.needsUpdate = true;
mesh.computeBoundingBox();
mesh.computeBoundingSphere();
Gộp geometry tĩnh
Với vật thể không bao giờ di chuyển, dùng chung material nhưng khác hình, gộp chúng thành một geometry lúc dựng:
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
wall.updateMatrix(); floor.updateMatrix(); pillar.updateMatrix();
const merged = mergeGeometries([
wallGeo.clone().applyMatrix4(wall.matrix),
floorGeo.clone().applyMatrix4(floor.matrix),
pillarGeo.clone().applyMatrix4(pillar.matrix),
]);
scene.add(new THREE.Mesh(merged, sharedMaterial)); // 1 draw call for the lot
Instancing dành cho cùng hình, nhiều transform; gộp dành cho geometry tương thích, bố cục tĩnh. Phải bake local transform trước khi merge, nếu không mọi hình chồng tại origin. Gộp còn làm culling thô hơn vì nhiều vật dùng chung một bounding volume — giảm calls nhưng có thể tăng số tam giác phải vẽ.
Culling và mức chi tiết
Three.js đã làm frustum culling — vật thể ngoài tầm nhìn camera bị bỏ qua tự động. Bạn có thể hỗ trợ bằng cách giữ bounding volume đúng, và chỉ tắt nó cho những thứ như skybox không nên bị cull.
Với vật thể ở xa, đổi sang phiên bản rẻ hơn bằng LOD:
const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0); // < 15 units away
lod.addLevel(midDetailMesh, 15);
lod.addLevel(lowDetailMesh, 40); // far away → simplest mesh
scene.add(lod); // Three.js picks the level by camera distance
Dispose — rò rỉ không ai thấy
Tài nguyên GPU (geometry, material, texture, render target) không được thu gom rác khi bạn scene.remove() một vật thể. Bạn phải dispose tường minh, nếu không VRAM tăng dần tới khi tab sập — nhất là trong SPA mount/unmount scene:
// Chỉ gọi khi scope này sở hữu độc quyền các resource dưới đây.
function disposeOwnedMesh(mesh) {
mesh.geometry.dispose();
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
for (const m of mats) {
for (const key in m) { const v = m[key]; if (v && v.isTexture) v.dispose(); }
m.dispose();
}
}
Đoạn trên không an toàn cho resource dùng chung: dispose texture của một mesh có thể phá mesh khác. Production cần asset cache/ref-count hoặc resource scope biết ai sở hữu gì (Part 21). Theo dõi renderer.info.memory.geometries/textures qua cùng một chu kỳ mount → use → unmount; xu hướng tăng lặp lại là tín hiệu điều tra, không tự nó chứng minh số byte VRAM bị leak.
Checklist hiệu năng của senior
- Tái dùng geometry và material — không tạo trong loop hay mỗi frame.
- Instance vật lặp; gộp vật tĩnh.
- Đặt pixel-ratio cap theo quality tier; 2 không phải con số đúng cho mọi viewport/device.
- Dùng LOD và để frustum culling làm việc.
- Dispose mọi thứ bạn gỡ.
- Đo nhiều lớp —
renderer.infocho counter; DevTools/frame capture/GPU timer cho thời gian.
Lời cảnh báo của sư phụ
- FPS thấp với geometry đơn giản? Giảm object count để kiểm tra giả thuyết CPU submission; đừng kết luận trước khi tách CPU/GPU.
- Instance dồn hết ở gốc? Bạn quên
instanceMatrix.needsUpdate = true. - Bộ nhớ tăng dần? Bạn gỡ vật thể mà không dispose tài nguyên GPU.
- Instancing không giúp? Vật thể của bạn khác material — instancing cần một material chung.
Luyện tập, không thì coi như chưa học
- Tìm vách: chế độ separate — tăng số lượng tới khi FPS dưới 30, ghi lại, rồi đổi sang instanced cùng số lượng.
- Đọc counter: log
renderer.info.render.callsở cả hai chế độ, rồi bật shadow/post pass và quan sát vì sao tổng frame không còn chỉ là 1 và N. - Săn rò rỉ: dựng lại scene liên tục mà không dispose và xem
memory.geometriesleo.
Phần tiếp theo
Scene của bạn nhanh và đẹp — nhưng vẫn thụ động; người dùng chỉ xoay được. Ở Phần 9 ta làm chúng tương tác: Raycaster để chọn vật dưới con trỏ, xử lý hover và click, điều khiển kéo, và biến scene 3D thành UI thật — với demo cho bạn hover và chọn vật trong lưới.