renderer.info.render counts the last frame's draw calls and triangles, info.memory counts resources and info.programs lists compiled programs. For GPU time, WebGPURenderer reads timestamp queries when created with trackTimestamp: true:
import { WebGPURenderer, PMREMGenerator } from 'three/webgpu';
import * as BN from './bookshelf.js';
const renderer = new WebGPURenderer({ antialias: true, trackTimestamp: true });
await renderer.init();
const stage = BN.createStage({ width: 640, height: 240, renderer });
stage.scene.add(BN.createBookcase());
BN.lightBookcase(stage, PMREMGenerator);
const gpuTimes = [];
for (let frame = 0; frame < 5; frame++) {
renderer.render(stage.scene, stage.camera);
gpuTimes.push(await renderer.resolveTimestampsAsync('render')); // milliseconds
}
console.log('GPU ms per frame:', gpuTimes.map((ms) => ms.toFixed(3)).join(' '));
window.__done = true;Output
GPU ms per frame: 7.340 0.393 0.459 0.459 0.524
After a first frame of one-time work, the bookcase needs about half a millisecond of GPU time on this shared GTX 1650. Other tools:
| Tool | Shows |
|---|---|
| Chrome DevTools 3,948 Performance panel | CPU time per frame, long tasks, GPU activity |
| three/addons/inspector/Inspector.js | Performance, memory and timeline panels for WebGPURenderer |
| Spector.js 0.9.33 1,619 (github.com/BabylonJS/Spector.js (https://github.com/BabylonJS/Spector.js 1,619 ), MIT) | Every WebGL call and state in a captured frame |
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 12px system-ui, sans-serif; color: #263238; }
#stage { position: relative; width: 600px; max-width: 100%; }
canvas { display: block; }
#panel { position: absolute; left: 6px; top: 6px; background: rgb(20 24 28 / 0.85); color: #d7f0dd; padding: 6px; font: 11px monospace; }
#panel canvas { margin-top: 4px; }
#stats { white-space: pre; }
.ui { padding: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div id="stage"><div id="panel"><div id="stats"></div><canvas id="graph" width="160" height="40"></canvas></div></div>
<div class="ui"><button id="more">Add 100 books (separate meshes)</button> <button id="reset">Reset</button></div>
<script type="module">
import * as THREE from 'three';
const colors = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 260);
document.getElementById('stage').prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(40, 600 / 260, 0.1, 50);
camera.position.set(0, 1.5, 5);
camera.lookAt(0, 0, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);
const group = new THREE.Group();
scene.add(group);
const geometry = new THREE.BoxGeometry(0.1, 0.4, 0.3); // shared: one geometry on the GPU
const materials = colors.map((color) => new THREE.MeshStandardMaterial({ color }));
function addBooks(n) {
for (let i = 0; i < n; i++) {
const book = new THREE.Mesh(geometry, materials[group.children.length % 6]);
const k = group.children.length;
book.position.set(((k * 0.137) % 5) - 2.5, Math.floor(k / 36) * 0.45 - 1, ((k * 0.61) % 2) - 1);
book.rotation.y = k;
group.add(book);
}
}
addBooks(60);
document.getElementById('more').onclick = () => addBooks(100);
document.getElementById('reset').onclick = () => { group.clear(); addBooks(60); };
const graph = document.getElementById('graph').getContext('2d');
const times = [];
let last = performance.now();
renderer.setAnimationLoop((now) => {
group.rotation.y = now / 4000;
const start = performance.now();
renderer.render(scene, camera);
const cpu = performance.now() - start; // CPU time spent in render()
times.push(now - last); last = now;
if (times.length > 80) times.shift();
const { calls, triangles } = renderer.info.render, { geometries, textures } = renderer.info.memory;
document.getElementById('stats').textContent =
`meshes ${group.children.length} calls ${calls} tris ${triangles}\n`;
document.getElementById('stats').textContent +=
`geometries ${geometries} textures ${textures} programs ${renderer.info.programs.length} render() ${cpu.toFixed(1)} ms`;
graph.clearRect(0, 0, 160, 40);
graph.fillStyle = '#7ad27a';
times.forEach((ms, i) => { const h = Math.min(40, ms); graph.fillRect(i * 2, 40 - h, 2, h); });
graph.fillStyle = '#fff'; graph.fillRect(0, 40 - 16.7, 160, 1); // the 60 fps line
});
</script>