[](https://www.npmjs.com/package/stats-gl) [](https://www.npmjs.com/package/st
npm install stats-gl> For AI/LLM users: See llms.txt for a condensed API reference.
WebGL/WebGPU Performance Monitor with real-time FPS, CPU, and GPU timing. Supports Three.js, native WebGL2/WebGPU, Web Workers, and texture preview panels.
https://github.com/RenaudRohlinger/stats-gl/assets/15867665/3fdafff4-1357-4872-9baf-0629dbaf9d8c
``bash`
npm install stats-gl
`js
import Stats from 'stats-gl';
import * as THREE from 'three';
const stats = new Stats({ trackGPU: true });
document.body.appendChild(stats.dom);
const renderer = new THREE.WebGLRenderer(); // or WebGPURenderer
stats.init(renderer);
function animate() {
renderer.render(scene, camera); // or renderAsync for WebGPU
stats.update();
}
renderer.setAnimationLoop(animate);
`
`js
import Stats from 'stats-gl';
const stats = new Stats({ trackGPU: true });
const canvas = document.querySelector('#canvas');
stats.init(canvas);
document.body.appendChild(stats.dom);
function animate() {
stats.begin();
// ... your WebGL draw calls ...
stats.end();
stats.update();
requestAnimationFrame(animate);
}
animate();
`
`js
import Stats from 'stats-gl';
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice({ requiredFeatures: ['timestamp-query'] });
const context = canvas.getContext('webgpu');
const stats = new Stats({ trackGPU: true });
stats.init(device); // Pass the GPUDevice
document.body.appendChild(stats.dom);
function animate() {
stats.begin();
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [...],
timestampWrites: stats.getTimestampWrites() // Enable GPU timing
});
// ... your draw calls ...
pass.end();
stats.end(encoder); // Pass encoder to resolve timestamps
device.queue.submit([encoder.finish()]);
stats.update();
requestAnimationFrame(animate);
}
animate();
`
A component is available through @react-three/drei:
`jsx
import { Canvas } from '@react-three/fiber'
import { StatsGl } from '@react-three/drei'
const Scene = () => (
)
`
A component is available through cientos:
`vue
`
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| trackFPS | boolean | true | Enable built-in FPS and CPU panels |trackGPU
| | boolean | false | Enable GPU timing (requires extension support) |trackHz
| | boolean | false | Enable refresh rate detection |trackCPT
| | boolean | false | Enable Three.js compute shader timing (WebGPU only) |logsPerSecond
| | number | 4 | How often to update text display |graphsPerSecond
| | number | 30 | How often to update graphs |samplesLog
| | number | 40 | Number of samples for text averaging |samplesGraph
| | number | 10 | Number of samples for graph averaging |precision
| | number | 2 | Decimal places for CPU/GPU values |minimal
| | boolean | false | Minimal mode - click to cycle panels |horizontal
| | boolean | true | Horizontal panel layout |mode
| | number | 0 | Initial panel (0=FPS, 1=CPU, 2=GPU) |
stats-gl supports rendering in a Web Worker using OffscreenCanvas. Use StatsProfiler in the worker to collect timing data, and send it to the main thread where Stats displays it.
Worker (offscreen rendering):
`js
import { StatsProfiler } from 'stats-gl';
const profiler = new StatsProfiler({ trackGPU: true });
self.onmessage = async (e) => {
if (e.data.type === 'init') {
const canvas = e.data.canvas;
const gl = canvas.getContext('webgl2');
await profiler.init(gl);
requestAnimationFrame(loop);
}
};
function loop() {
profiler.begin();
// ... your rendering code ...
profiler.end();
profiler.update();
// Send timing data to main thread
self.postMessage({ type: 'stats', ...profiler.getData() });
requestAnimationFrame(loop);
}
`
Main thread:
`js
import Stats from 'stats-gl';
const stats = new Stats({ trackGPU: true });
document.body.appendChild(stats.dom);
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('worker.js', { type: 'module' });
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
worker.onmessage = (e) => {
if (e.data.type === 'stats') {
stats.setData(e.data);
}
};
function loop() {
stats.update();
requestAnimationFrame(loop);
}
loop();
`
StatsProfiler is a headless version of Stats designed for workers:
- init(canvas | device) - Initialize with WebGL context, OffscreenCanvas, or GPUDevicebegin()
- / end(encoder?) - Wrap your render calls (pass encoder for native WebGPU)getTimestampWrites()
- - Get timestampWrites config for native WebGPU render passupdate()
- - Process timing datagetData()
- - Returns { fps, cpu, gpu, gpuCompute }captureTexture(source, sourceId)
- - Capture texture to ImageBitmap for transfer
Use stats.setData(data) to feed external timing data into the Stats UI. When set, update() uses this data instead of internal timing.
Display render target previews alongside performance metrics. Supports both WebGL and WebGPU.
`js
const stats = new Stats({ trackGPU: true });
stats.init(renderer);
// Create a texture panel
const panel = stats.addTexturePanel('GBuffer');
// Set texture source (WebGLRenderTarget or WebGPU RenderTarget)
const renderTarget = new THREE.WebGLRenderTarget(width, height);
stats.setTexture('GBuffer', renderTarget);
// In render loop - textures update automatically
function animate() {
renderer.setRenderTarget(renderTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null);
renderer.render(scene, camera);
stats.update();
}
`
`js
// Worker - capture and transfer texture
const bitmap = await profiler.captureTexture(renderTarget, 'gbuffer');
self.postMessage(
{ type: 'texture', name: 'GBuffer', bitmap, width, height },
[bitmap]
);
// Main thread - receive and display
worker.onmessage = (e) => {
if (e.data.type === 'texture') {
stats.setTextureBitmap(e.data.name, e.data.bitmap, e.data.width, e.data.height);
}
};
`
- stats.addTexturePanel(name) - Create a new texture preview panelstats.setTexture(name, source)
- - Set Three.js RenderTarget sourcestats.setTextureWebGL(name, framebuffer, width, height)
- - Set raw WebGL framebufferstats.setTextureBitmap(name, bitmap, width?, height?)
- - Set ImageBitmap (for workers)stats.removeTexturePanel(name)
- - Remove a texture panel
Capture any Three.js TSL node for live preview. Works with MRT, post-processing, and custom shaders.
`js
import Stats from 'stats-gl';
import { statsGL } from 'stats-gl/addons/StatsGLNode.js';
import { addMethodChaining } from 'three/tsl';
// Enable .toStatsGL() method on TSL nodes
addMethodChaining('toStatsGL', statsGL);
const stats = new Stats({ trackGPU: true });
stats.init(renderer);
document.body.appendChild(stats.dom);
// In your PostProcessing setup:
const scenePass = pass(scene, camera);
scenePass.setMRT(mrt({
output,
normal: directionToColor(normalView),
diffuse: diffuseColor
}));
// Register nodes for capture - panels are created automatically
scenePass.getTextureNode('diffuse').toStatsGL('Diffuse', stats);
scenePass.getTextureNode('normal').toStatsGL('Normal', stats);
scenePass.getLinearDepthNode().toStatsGL('Depth', stats);
`
The same StatsGLNode.js addon works in Web Workers with OffscreenCanvas:
Worker:
`js
import { StatsProfiler } from 'stats-gl';
import { flushCaptures } from 'stats-gl/addons/StatsGLNode.js';
const profiler = new StatsProfiler({ trackGPU: true });
await profiler.init(renderer);
// Register nodes (no stats instance needed in worker)
diffuseNode.toStatsGL('Diffuse');
normalNode.toStatsGL('Normal');
depthNode.toStatsGL('Depth');
async function render() {
profiler.begin();
postProcessing.render();
profiler.end();
profiler.update();
// Send stats to main thread
self.postMessage({ type: 'stats', data: profiler.getData() });
// Capture and transfer TSL nodes as ImageBitmaps
const captures = await flushCaptures(renderer);
for (const { name, bitmap } of captures) {
self.postMessage({ type: 'texture', name, bitmap }, [bitmap]);
}
}
`
Main Thread:
`js
import Stats from 'stats-gl';
const stats = new Stats({ trackGPU: true });
document.body.appendChild(stats.dom);
// Create panels for worker captures
stats.addTexturePanel('Diffuse');
stats.addTexturePanel('Normal');
stats.addTexturePanel('Depth');
worker.onmessage = (e) => {
if (e.data.type === 'stats') stats.setData(e.data.data);
if (e.data.type === 'texture') {
stats.setTextureBitmap(e.data.name, e.data.bitmap);
}
};
`
Use a callback to transform the node before capture (e.g., linearize depth):
`js`
depthNode.toStatsGL('Depth', stats, (node) => linearizeDepth(node));
Add custom metrics panels:
`js
const customPanel = stats.addPanel(new Stats.Panel('COUNT', '#ff0', '#220'));
function animate() {
// Update with value and max
customPanel.update(currentValue, maxValue, 2); // 2 decimal places
customPanel.updateGraph(currentValue, maxValue);
stats.update();
}
`
Main class with DOM rendering.
`js
import Stats from 'stats-gl';
const stats = new Stats(options);
stats.init(renderer); // Initialize with Three.js renderer, canvas, or GPUDevice
stats.begin(); // Start timing (auto-called for Three.js)
stats.end(encoder?); // End timing (pass encoder for native WebGPU)
stats.update(); // Update display
stats.setData(data); // Set external timing data
stats.getTimestampWrites(); // Get timestampWrites for native WebGPU render pass
stats.dispose(); // Clean up resources
`
`js
import Stats, {
StatsProfiler, // Headless profiler for workers
PanelTexture, // Texture preview panel class
TextureCaptureWebGL, // WebGL texture capture utility
TextureCaptureWebGPU, // WebGPU texture capture utility
StatsGLCapture // Addon capture helper
} from 'stats-gl';
// TSL Node capture addon (WebGPU only, works in main thread and workers)
import { statsGL, flushCaptures } from 'stats-gl/addons/StatsGLNode.js';
``
Contributions to stats-gl are welcome. Please report any issues or bugs you encounter.
This project is licensed under the MIT License.