Profiling

Profiling with Renderer Info and DevTools

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:

GPU time per frame from WebGPU timestamp queriesJavaScript
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:

Profiling tools for Three.js 30,815 pages
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
A live renderer.info panel with a frame-time graph while books are added to the sceneHTMLLive
<!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>