关于 threejs-performance
面向 Three.js 开发者的性能优化技能,涵盖 WebGL 与 WebGPU 场景的测量和调优。内容从渲染器统计数据出发,介绍减少绘制、显存和 CPU 开销的方法。
主要功能
- 01测量渲染器统计数据与帧时间
- 02通过合并和实例化减少绘制调用
- 03管理 GPU 纹理与几何体内存
- 04调整像素比、透明度与阴影
- 05减少逐帧 CPU 变换工作
适用场景
- 01排查绘制调用或三角形瓶颈
- 02优化场景中的重复物体
- 03降低纹理显存占用
- 04改善高 DPI 屏幕上的渲染开销
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-performance安装后在 Claude Code 中输入/threejs-performance确认已加载。
详细介绍
Three.js 性能
针对 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
快速开始
import * as THREE from "three";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0xffffff, 0x445566, 1));
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(3, 6, 4);
scene.add(sun);
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);
const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16), new THREE.MeshStandardMaterial({ color: 0x4488ff }));
scene.add(mesh);
function animate() {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
// the counters reset at the start of every render() call, so these cover the frame just drawn
console.log(`${renderer.info.render.calls} calls, ${renderer.info.render.triangles} triangles, ${renderer.info.memory.textures} textures`);
}
renderer.setAnimationLoop(animate);
先测量
优化数据指出的问题,而不是凭直觉猜测的问题。调用 render() 后读取 WebGLRenderer 的 renderer.info 对象:
function animate() {
renderer.render(scene, camera);
console.table({
calls: renderer.info.render.calls, // draw calls submitted by this render
triangles: renderer.info.render.triangles,
lines: renderer.info.render.lines,
points: renderer.info.render.points,
geometries: renderer.info.memory.geometries, // geometry buffers live on the GPU
textures: renderer.info.memory.textures,
programs: renderer.info.programs.length, // compiled shader programs in the cache
});
}
renderer.setAnimationLoop(animate);
阅读这些数据时,值得注意以下几点:
- 当
info.autoReset为 true(默认值)时,计数器会在每次render()调用开始时重置。在render()之前读取会得到上一帧的数据;一帧中有多次render()调用(拾取、反射、后期处理通道)时,只会显示最后一次调用的数据。 - 阴影贴图绘制也会使用同一组计数器:每个投射阴影的光源都会在
render()中从光源视角重新绘制其投射阴影的物体,这些绘制会计入calls和triangles。切换光源的castShadow时,这里会直接显示变化。 info.memory.geometries和info.memory.textures统计的是上传次数,而非字节数;对应的dispose()执行后,计数会减少。几何体和纹理只会上传一次,并由引用它们的所有网格共享。
如果一帧确实有多次 render() 调用,请自行累加整帧的数据:
renderer.info.autoReset = false; // render() stops clearing the counters
function animate() {
renderer.render(scene, camera);
console.log(renderer.info.render.calls); // every render of this frame, summed
renderer.info.reset(); // once per frame, after the last render
}
renderer.setAnimationLoop(animate);
Timer 会提供帧间隔;对一秒内的多帧取平均值,因为单帧抖动太大,不适合用于比较。该间隔不会低于一次显示器刷新周期(60 Hz 时为 16.7 ms),因此它能显示掉帧情况,但无法反映性能余量;使用 performance.now() 在帧处理代码前后计时,以测量 CPU 端的帧耗时:
const timer = new THREE.Timer();
timer.connect(document); // pauses while the tab is hidden
let interval = 0;
let cpu = 0;
let frames = 0;
function animate() {
timer.update();
interval += timer.getDelta(); // seconds since the previous frame, never less than one display refresh
const start = performance.now();
renderer.render(scene, camera);
cpu += performance.now() - start; // CPU time spent in render(); the GPU executes the commands later
frames++;
if (frames === 60) {
console.log(`${((interval / frames) * 1000).toFixed(2)} ms per frame, ${(cpu / frames).toFixed(2)} ms CPU in render()`);
interval = 0;
cpu = 0;
frames = 0;
}
}
renderer.setAnimationLoop(animate);
如果要深入了解帧耗时以外的情况,请使用浏览器性能分析器:Chrome DevTools 的 Performance 面板会显示主线程上的长任务、脚本执行与渲染耗时,以及 GPU 轨道上的 GPU 工作情况。需要始终显示数据时,stats-gl 包可以提供实时的逐帧统计信息叠加层。
绘制调用
每个可见网格会排入一次绘制调用;如果材质是数组,则每个几何体组排入一次。两项设置会使调用次数倍增:带有 side: THREE.DoubleSide 的 transparent 材质会将每个网格绘制两次,先绘制背面,再绘制正面(设置 forceSinglePass: true 可使其只绘制一次);只要视野中有任何 transmission 大于 0 的 MeshPhysicalMaterial,每个不透明对象都会再次绘制到透射缓冲区中。没有这两种情况时,1,000 个使用单一材质的网格就对应 1,000 次绘制调用;应关注 info.render.calls。常见的优化方法:
合并静态几何体。 多个不会移动且共用一种材质的网格可以合并成一个网格和一次调用:
import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js";
const rockGeometry = new THREE.DodecahedronGeometry(1, 0);
const transform = new THREE.Matrix4();
const parts = [];
for (let i = 0; i < 200; i++) {
const baked = rockGeometry.clone();
transform.setPosition((Math.random() - 0.5) * 40, 0, (Math.random() - 0.5) * 40);
baked.applyMatrix4(transform); // bake the placement into the vertices
parts.push(baked);
}
const merged = BufferGeometryUtils.mergeGeometries(parts); // inputs need matching attributes and index state
scene.add(new THREE.Mesh(merged, new THREE.MeshNormalMaterial())); // 200 rocks, one call — but one bounding sphere for all of them
合并会以剔除粒度换取调用次数:合并后的网格只有一个包围球,因此视锥剔除只能全有或全无。应按簇合并世界大小的簇,而不要将整个关卡合并成一个网格。
实例化重复对象。 使用单个几何体绘制多次,并为每个实例设置变换,仍只需一次调用:
const tree = new THREE.ConeGeometry(0.5, 2, 6);
const trees = new THREE.InstancedMesh(tree, new THREE.MeshNormalMaterial(), 500);
const placement = new THREE.Object3D();
for (let i = 0; i < 500; i++) {
placement.position.set((Math.random() - 0.5) * 40, 1, (Math.random() - 0.5) * 40);
placement.rotation.y = Math.random() * 6.28;
placement.updateMatrix();
trees.setMatrixAt(i, placement.matrix);
}
// culling tests one sphere around all instances (computed lazily on the first cull test), so all 500 draw whenever it touches the view;
// after moving instances later, set trees.instanceMatrix.needsUpdate = true and call trees.computeBoundingSphere()
scene.add(trees);
批处理不同几何体。 多个共用一种材质的几何体可以打包到共享缓冲区中:
const material = new THREE.MeshNormalMaterial();
const batchedMesh = new THREE.BatchedMesh(500, 20000, 20000, material);
const coneId = batchedMesh.addGeometry(new THREE.ConeGeometry(0.5, 2, 6));
const boxId = batchedMesh.addGeometry(new THREE.BoxGeometry(1, 1, 1));
const placement = new THREE.Matrix4();
for (let i = 0; i < 500; i++) {
const instanceId = batchedMesh.addInstance(i % 2 ? coneId : boxId); // a geometry renders only through an instance
placement.makeTranslation((Math.random() - 0.5) * 40, 1, (Math.random() - 0.5) * 40);
batchedMesh.setMatrixAt(instanceId, placement);
}
scene.add(batchedMesh);
与 InstancedMesh 不同,它会分别剔除和排序每个实例(perObjectFrustumCulled 和 sortObjects 默认都为开启状态)。浏览器支持 WEBGL_multi_draw 时,WebGLRenderer 会通过多重绘制将它们合并为一次调用;否则每个实例各需一次绘制。WebGPU 后端则为每个实例各发出一次绘制调用。threejs-geometry 对这两种 API 都有深入介绍。
按距离降低细节。 LOD 会将每个级别都保留为子对象,并在每次 render() 时(默认开启 autoUpdate)仅显示与当前摄像机距离相对应的级别,因此远处对象提交的三角形更少——但调用次数不变:
const detailMaterial = new THREE.MeshNormalMaterial();
const lod = new THREE.LOD();
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 64, 32), detailMaterial), 0); // distances are camera-to-object
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 16, 8), detailMaterial), 12);
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 8, 4), detailMaterial), 30);
scene.add(lod);
让剔除跳过整个调用。 默认开启视锥剔除:网格排入队列前,会检测其几何体的包围球是否与摄像机视锥相交;检测失败则完全移除这次绘制。以下两种情况会导致剔除悄无声息地失效:在 CPU 上编辑顶点却不调用 geometry.computeBoundingSphere(),会留下过期的包围球——如果包围球小于移动后的顶点,即使网格仍在视野内也会被隐藏;在着色器中移动顶点则不会更新 CPU 端的包围球,因此应只对该网格设置 mesh.frustumCulled = false,而不是对整个场景设置。
GPU 内存
info.memory.textures 统计纹理数量;字节数需要自行计算。RGBA8 纹理每像素占 4 字节,默认 mip 链会使 WebGPURenderer 的字节估算乘以 1.333——一张 2048 × 2048 的颜色贴图约占 22 MB 的 VRAM。可从三个方面优化:
- 压缩 KTX2。 Basis Universal KTX2 纹理(ETC1S 或 UASTC)会转码为 GPU 支持的块压缩格式(ASTC、BC7、BC1 或 BC3、ETC2、ETC1 或 PVRTC),并在 VRAM 中保持压缩状态;PNG 或 JPEG 则会展开为每像素 4 字节。如果这些格式都不适用于该文件(ASTC 仅适用于 UASTC 文件,ETC1 不支持 Alpha,PVRTC 要求尺寸为 2 的幂),KTX2Loader 会回退到未压缩的 RGBA。使用 KTX2Loader 加载:
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";
const ktx2Loader = new KTX2Loader().detectSupport(renderer);
const map = await ktx2Loader.loadAsync("color.ktx2"); // a CompressedTexture for a Basis Universal file
map.colorSpace = THREE.SRGBColorSpace; // KTX2Loader already sets it from the file's transfer function; this line only matters for a color file tagged linear
mesh.material.map = map;
mesh.material.needsUpdate = true; // the material rendered without a map before
- 尺寸。 1024 尺寸纹理的占用是 2048 尺寸纹理的四分之一。创建颜色贴图时应采用其在屏幕上的显示尺寸,并让 mipmapping 处理距离变化。
- 释放资源。
geometry.dispose()会立即释放其 GPU 缓冲区;当没有其他未释放的纹理与之共享时,texture.dispose()会释放 GPU 纹理(texture.clone()会共享图像;只要 wrap、filter、format 和 color space 保持不变,也会共享其 GPU 副本)。统计信息不会统计网格,因此只释放不再参与渲染的资源——仍在使用已释放几何体或纹理的网格会在下次渲染时重新上传它:
scene.remove(mesh);
mesh.geometry.dispose(); // frees the GPU buffers now, even if another mesh still uses this geometry
// material.dispose() releases its shader programs (deleted once no material uses them), never its textures
for (const value of Object.values(mesh.material)) {
if (value?.isTexture) value.dispose();
}
mesh.material.dispose();
mesh.dispose(); // renderer-side state kept for this one object
console.log(renderer.info.memory.textures); // the count drops as each texture frees
要释放整个子树,请遍历它——object.dispose() 不会访问子对象。在 WebGPURenderer 下,memory.geometries 和 memory.textures 仍表示数量,而 *Size 字段和 memory.total 会提供字节数估算;请参阅 WebGPU 一节。
填充率
填充率指片元工作量:每帧着色的像素数。绘图缓冲区的尺寸为 width × ratio 乘以 height × ratio 像素,因此着色成本随像素比例的平方增长——3× 显示器着色的像素数是 1× 的 9 倍。限制比例:
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
过度绘制与透明度。 先绘制不透明几何体(依次按组、渲染顺序、材质和距离排序),然后绘制透射材质,最后绘制透明材质——透明材质按从后到前的顺序绘制,才能正确合成混合效果。堆叠的透明图层无法互相剔除:另一个透明表面后方的每一层仍会为其像素着色,而覆盖全屏的透明叠加层会在场景之上再为屏幕的每个像素着色一次。效果允许时,优先使用不透明材质,并尽量减少透明四边形的数量和尺寸。renderer.info 不会显示过度绘制——调用次数和三角形数保持不变——只有帧时间会变化。
阴影贴图。 开启阴影更新时,每个投射阴影的光源每帧都会将其投射体渲染到深度纹理中。默认尺寸较小(512 × 512);谨慎调高,并冻结静态场景的阴影贴图:
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(3, 6, 4);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
scene.add(sun);
renderer.shadowMap.enabled = true;
// static scenery: draw the shadow maps once instead of every frame
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true; // render them one more time, then freeze
// per light instead of globally: sun.shadow.autoUpdate = false; sun.shadow.needsUpdate = true;
每个投射阴影的光源都会增加一个渲染通道,这些绘制会显示在 info.render.calls 中:它会重新绘制阴影相机视锥内每个 castShadow 对象;对于 PointLight,会针对对象落入的每个立方体面绘制一次(最多六次);使用 VSMShadowMap 时,还会绘制 receiveShadow 对象,外加两次模糊绘制。收紧阴影相机范围(设置 sun.shadow.camera.left、right、top、bottom,然后调用 sun.shadow.camera.updateProjectionMatrix())可减少绘制的投射体;贴图尺寸决定每个渲染通道的填充成本。
循环中的 CPU 工作
静态变换。 每次调用 render() 都会调用 scene.updateMatrixWorld();当 matrixAutoUpdate 为 true(默认值)时,该函数会遍历每个对象,并根据位置、旋转和缩放重新组合其局部矩阵。对于永不移动的场景物体,只需组合一次:
mesh.matrixAutoUpdate = false;
mesh.updateMatrix(); // bake position/rotation/scale into mesh.matrix before the first render
// later, when this object does move:
mesh.position.set(1, 2, 3);
mesh.updateMatrix(); // manual now: the renderer no longer recomposes it
光线投射。 intersectObjects(objects, true) 会遍历每个对象的所有后代并逐一测试:先测试图层,再测试几何体的包围球,最后测试三角形。应缩小输入范围,而不是过滤输出:
const raycaster = new THREE.Raycaster();
raycaster.layers.set(1); // only objects enabled on layer 1 are tested, before any geometry work
const pickables = [];
scene.traverse((child) => {
if (child.isMesh && child.userData.pickable) {
child.layers.enable(1); // adds layer 1 while keeping layer 0 membership
pickables.push(child);
}
});
const hits = raycaster.intersectObjects(pickables, false); // an explicit list, no recursion
执行光线投射的 pointermove 处理程序会在每次事件触发时运行,因此在带有拾取功能的场景中会成为热点。对于大型静态几何体,three-mesh-bvh 包可加速三角形测试。
循环中不分配内存。 每帧创建的对象会成为垃圾,垃圾回收暂停会导致帧时间周期性飙升。应复用在循环外声明的向量和矩阵:
const timer = new THREE.Timer();
timer.connect(document);
const step = new THREE.Vector3(); // allocated once, reused every frame
const target = new THREE.Vector3(0, 1, 0);
function animate() {
timer.update();
step.subVectors(target, mesh.position).multiplyScalar(Math.min(timer.getDelta() * 2, 1));
mesh.position.add(step);
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
着色器编译卡顿
着色器程序采用延迟构建:材质首次在此前未遇到的条件下渲染时——例如光源数量不同,或为其开启了雾效——会因编译着色器而卡顿。应在加载期间构建:
// during loading, before the first frame: compile everything the scene will render with
await renderer.compileAsync(scene, camera); // uses KHR_parallel_shader_compile where available
renderer.setAnimationLoop(() => {
renderer.render(scene, camera); // material programs are ready; shadow-depth programs still build on the first shadow render
});
renderer.compile(scene, camera) 会触发相同的构建,但会立即返回,因此首次渲染仍会等待尚未完成的程序;优先使用异步形式。更改会编译进着色器的设置(transparent、side、blending、flatShading、vertexColors、dithering、premultipliedAlpha、alphaHash,以及添加或移除贴图)时,需要设置 material.needsUpdate = true 并构建新的程序,因此应避免在运行时切换这些设置(threejs-materials 列出了所有此类设置)。
WebGPU 与 TSL
import * as THREE from "three/webgpu";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111); // the WebGPU canvas is transparent by default
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 4, 18);
function animate() {
renderer.render(scene, camera);
// autoReset clears per-frame counters at each loop tick; render.calls counts render passes since the app started
console.log(`${renderer.info.render.drawCalls} draw calls, ${renderer.info.memory.total} bytes of GPU memory`);
}
renderer.setAnimationLoop(animate);
计算将粒子处理从 CPU 转移出去:无需逐粒子循环,无需每帧重新上传缓冲区,一次调度即可更新所有粒子:
import * as THREE from "three/webgpu";
import { Fn, cos, hash, instanceIndex, instancedArray, sin, time, TWO_PI, vec3 } from "three/tsl";
const count = 100000;
const positionBuffer = instancedArray(count, "vec3"); // a storage buffer living on the GPU
const simulate = Fn(() => {
const rand = hash(instanceIndex);
const angle = rand.mul(TWO_PI).add(time); // time updates itself every frame
positionBuffer.element(instanceIndex).assign(vec3(cos(angle).mul(8), sin(rand.mul(97).add(time.mul(2))).mul(2).add(2), sin(angle).mul(8)));
});
const simulateCompute = simulate().compute(count); // one compute thread per particle
const material = new THREE.SpriteNodeMaterial();
material.positionNode = positionBuffer.toAttribute(); // the material reads the storage buffer directly
material.colorNode = vec3(1, 0.7, 0.3);
const particles = new THREE.Sprite(material);
particles.count = count; // the sprite draws once per buffer element
particles.frustumCulled = false; // culling tests one small sphere at the sprite origin, not the particles
scene.add(particles);
function animate() {
renderer.compute(simulateCompute); // GPU updates the buffer, then the draw reads it
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
- WebGPU 计数器使用各自的名称:
info.render.drawCalls表示每帧的绘制调用数,info.render.calls(渲染通道:每次render()以及输出通道等内部通道)和info.calls(渲染与计算调用数)表示自应用启动以来的累计值,info.compute.frameCalls表示每帧的计算调用数,info.memory.total则是顶点、索引和存储缓冲区、纹理、统一缓冲区及程序所占内存的估算值(memory.texturesSize、memory.attributesSize和其他*Size字段包含各部分的值)。压缩纹理在此处按 1 字节计,这是一个回退估算值,因此 KTX2 的内存占用会被低估。 - 第二个代码块中的循环替换第一个代码块中的
animate;由于已经运行await renderer.init(),因此renderer.compute()会直接提交。
常见错误
function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); }→renderer.setAnimationLoop(animate);:WebXR 会话只运行传给 setAnimationLoop 的回调,并通过会话自己的帧循环执行;在 WebGPURenderer 下,setAnimationLoop 还会在第一帧前等待 init() 完成。const clock = new THREE.Clock(); const dt = clock.getDelta(); const t = clock.getElapsedTime();→const timer = new THREE.Timer(); timer.update(); const dt = timer.getDelta(); const t = timer.getElapsed();:Clock 在 r183 中已弃用,而且 getElapsedTime() 内部会调用 getDelta(),因此同一帧稍后调用 getDelta() 时返回值接近于零。console.log(renderer.info.render.calls); renderer.render(scene, camera);→renderer.render(scene, camera); console.log(renderer.info.render.calls);:计数器会在 render() 开始时重置,因此在此之前读取会得到上一帧的数据。renderer.info.autoReset = false→renderer.info.autoReset = false; function animate() { renderer.render(scene, camera); console.log(renderer.info.render.calls); renderer.info.reset(); }:关闭 autoReset 后,除非每帧在最后一次 render() 后调用一次 reset(),否则计数器不会清零,而是逐帧累加。webgpuRenderer.info.render.calls→webgpuRenderer.info.render.drawCalls:在 WebGPURenderer 下,render.calls 统计应用启动以来的渲染通道数,而 drawCalls 统计当前帧的绘制调用数;WebGLRenderer 没有 drawCalls,其 render.calls 统计每次 render() 的绘制数。renderer.compile(scene, camera)→await renderer.compileAsync(scene, camera):compile() 会将构建任务加入队列后立即返回,因此紧接着进行的渲染仍会因未完成的程序而阻塞;compileAsync() 则仅在 KHR_parallel_shader_compile 报告所有程序均已完成时才会解析,前提是浏览器提供该扩展。renderer.shadowMap.type = THREE.PCFSoftShadowMap→renderer.shadowMap.type = THREE.PCFShadowMap:PCFSoftShadowMap 在 r182 中已弃用,并会回退到 PCF,同时在控制台发出警告。mesh.material.dispose()→mesh.material.dispose(); for (const value of Object.values(mesh.material)) if (value?.isTexture) value.dispose();:material.dispose() 会释放其着色器程序,但不会释放材质的纹理;只有对每个纹理调用 texture.dispose(),info.memory.textures 才会减少。gltf.scene.dispose()→gltf.scene.traverse((child) => { child.geometry?.dispose(); for (const material of [child.material ?? []].flat()) { for (const value of Object.values(material)) if (value?.isTexture) value.dispose(); material.dispose(); } child.dispose(); }):Object3D.dispose() 只会释放该对象自身的渲染器状态,不会释放其子对象、几何体、材质或纹理。const hits = raycaster.intersectObjects(scene.children, true)→raycaster.layers.set(1); const hits = raycaster.intersectObjects(pickables, false);:递归调用会在每次事件中遍历并检测每个后代对象;而 layers 检查会在每个对象进行几何体检测前运行,使用明确的列表则完全不会遍历场景中的其余对象。scene.traverse((child) => (child.frustumCulled = false))→geometry.computeBoundingSphere(); // after moving vertices on the CPU:裁剪会通过每个几何体的包围球跳过整个绘制调用,因此在整个场景中禁用裁剪会导致每帧都绘制所有内容。function animate() { mesh.position.add(new THREE.Vector3(0, 0.01, 0)); }→const offset = new THREE.Vector3(0, 0.01, 0); function animate() { mesh.position.add(offset); }:每帧分配的对象都会成为垃圾,需要垃圾回收器暂停运行来回收,造成周期性的帧耗时尖峰。const dt = uniform(0); /* fed from the timer each frame */→const dt = deltaTime;:内置的 time 和 deltaTime 节点是渲染器在每次渲染时都会更新的 uniform,因此手动传入副本会造成重复,还需要单独使用 Timer。
另请参阅
threejs-fundamentals- 渲染器构建、渲染循环和尺寸调整处理,为此处测量的一切提供基础。threejs-geometry- 绘制调用优化背后的完整实例化、批处理、合并和属性 API。threejs-materials- 会编译进着色器并触发重建的材质设置。threejs-loaders- 为这些优化手段生成的压缩文件配置 KTX2、Draco 和 meshopt 加载器。threejs-lighting- 此技能会测量其开销的光源类型、单位和阴影设置。threejs-postprocessing- 合成器自身的缓冲区、MSAA 和像素比设置。threejs-asset-pipeline- 生成 KTX2 纹理以及压缩、实例化或合并后的 glTF 文件的 gltf-transform 命令。