r3f-monitor
v2.2.1
Published
A performance monitor for React Three Fiber. Track FPS, draw calls, memory, and GPU usage.
Downloads
1,935
Maintainers
Readme
R3F-Monitor
An advanced, easy-to-use performance monitoring tool for @react-three/fiber applications.
Add the <PerfMonitor /> component anywhere in your R3F Canvas — or go headless and bring your own UI.
Display Modes
🗂 Tab Display (Default)
A modern, structured interface with separated tabs for better focus and deep inspection.
📊 Classic Display
A compact, always-visible performance panel inspired by traditional FPS monitors.
🧪 Example
Live example:
👉 https://codesandbox.io/p/sandbox/3sqpy4
🚀 Key Features
- Comprehensive Metrics: Monitor FPS, CPU/GPU render times, and JS Heap Memory.
- Accurate Measurement Engine (v2): FPS via a real 1-second sliding window, CPU via
performance.now()accumulation, and GPU via a WebGL2 timer-query queue (EXT_disjoint_timer_query_webgl2) reporting true milliseconds. - Headless Mode (v2.1): Run the measurement engine without the built-in UI. Read live metrics with
usePerfData(). - Adaptive Quality (v2.2):
<PerfAdaptive />automatically raises/lowers a qualityfactor(0–1) based on sustained FPS — drei<PerformanceMonitor>-compatible API, plus real GPU/CPU times to tell GPU-bound from CPU-bound. Pair withuseGpuTier()for a per-device starting quality. - VRAM Estimation: Get an estimated breakdown of your GPU memory usage (Textures and Geometries).
- Deep Analysis: Inspect individual WebGL programs, toggle visibility, and track matrix updates.
- Flexible UI: Choose between graphical visualizations, detailed lists, or a minimal condensed view.
Note: Overclock mode was removed in v2. It still exists in
1.2.0and earlier — pin to1.2.0if you rely on it.
📦 Installation
# npm
npm install r3f-monitor
# yarn
yarn add r3f-monitor
# pnpm
pnpm add r3f-monitor
# bun
bun i r3f-monitor
⚙️ Options
logsPerSecond?: number // Log refresh rate (default: 10)
antialias?: boolean // Enable text antialiasing
deepAnalyze?: boolean // Enable detailed WebGL program inspection
showGraph?: boolean // Toggle performance graphs (default: true)
graphType?: "line" | "bar" // Graph style (default: "bar")
minimal?: boolean // Compact condensed view
matrixUpdate?: boolean // Count matrixWorld updates per frame
chart?: {
hz?: number // Graph refresh frequency (default: 60)
length?: number // Number of data points displayed (default: 120)
}
className?: string // Custom CSS class
style?: React.CSSProperties // Inline style override
position?:
| "top-right"
| "top-left"
| "bottom-right"
| "bottom-left" // Default: "top-right"
displayType?: "tab" | "classic" // Default: "tab"📐 Measurement Engine
Starting with v2, the FPS / CPU / GPU values come from a reworked measurement core:
- FPS — counted over a real 1-second sliding window and reported as a continuous value (smoothed with a light EMA), so the readout stays stable instead of flickering ±1.
- CPU — wall-clock time of the render phase, accumulated per frame via
performance.now(). - GPU — measured with a WebGL2 timer-query queue (
EXT_disjoint_timer_query_webgl2). Results are reported in true milliseconds (no scaling fudge). WebGL2 only.
The bar graph (graphType: "bar") scrolls left as new samples arrive and uses a fixed (high-water) vertical scale, with a gradient fill per metric.
🎛 Bring your own UI (Headless)
Added in v2.1
The default <PerfMonitor /> ships a ready-made UI. If you want to design your own
interface — or adjust quality based on FPS — use headless mode:
<PerfHeadless /> handles the measurement, you do the rest.
There are exactly two things you need:
- Read the values to display →
usePerfData() - Change quality based on FPS →
<PerfAdaptive />(see Adaptive Quality)
The one rule: place <PerfHeadless /> somewhere inside <Canvas>.
1) Read the values — usePerfData()
A single hook, updating at logsPerSecond (default ~10×/sec, not every frame). The
component reading the data can live outside <Canvas> — it only reads the store,
no useThree required.
Call with no args → returns all the metrics:
const {
fps,
cpu,
gpu,
mem,
vram, // the "hot" numbers
gl, // { calls, triangles, points, lines, geometries, textures, programs }
infos, // { version, renderer, vendor }
} = usePerfData();| Field | Unit | Meaning |
| ------- | -------- | ------------------------------------------ |
| fps | frames/s | frames per second |
| cpu | ms | CPU time per frame |
| gpu | ms | GPU time per frame (WebGL2) |
| mem | MB | JS heap in use |
| vram | MB | estimated VRAM from the scene |
| gl | — | render stats for the latest frame |
| infos | — | renderer/vendor (constant for the session) |
Or pass a selector to read only the field you need — the component only re-renders when that field changes (avoids wasted re-renders when displaying a single number):
const fps = usePerfData((d) => d.fps);import { Canvas } from "@react-three/fiber";
import { PerfHeadless, usePerfData } from "r3f-monitor";
// Custom UI — place OUTSIDE <Canvas>
function MyHud() {
const { fps, cpu, gpu, mem, vram, gl } = usePerfData();
return (
<div
style={{ position: "fixed", top: 12, left: 12, font: "12px monospace" }}
>
<div>{fps.toFixed(0)} FPS</div>
<div>
CPU {cpu.toFixed(1)} ms · GPU {gpu.toFixed(1)} ms
</div>
<div>
RAM {mem.toFixed(0)} MB · VRAM {vram.toFixed(1)} MB
</div>
<div>
{gl.calls} calls · {gl.triangles} tris
</div>
</div>
);
}
export default function App() {
return (
<>
<Canvas>
<PerfHeadless logsPerSecond={10} />
{/* <YourScene /> */}
</Canvas>
<MyHud />
</>
);
}🎚 Adaptive Quality
Added in v2.2
<PerfAdaptive /> watches the sustained FPS coming from the measurement engine and
maintains a quality factor between 0 and 1. When the device comfortably exceeds
the upper FPS bound the factor steps up (incline); when it stays under the lower
bound it steps down (decline). You map the factor to whatever costs performance:
dpr, shadow map size, post-processing, draw distance…
The API mirrors drei's <PerformanceMonitor>, so existing code ports 1:1.
The one rule: mount a measuring component — <PerfHeadless /> or <PerfMonitor /> —
inside <Canvas>. <PerfAdaptive /> itself renders nothing and only listens
(1 FPS sample per engine log tick, ~10/s by default).
import { useState } from "react";
import { Canvas } from "@react-three/fiber";
import { PerfHeadless, PerfAdaptive } from "r3f-monitor";
export default function App() {
const [dpr, setDpr] = useState(1);
return (
<Canvas dpr={dpr}>
<PerfHeadless />
<PerfAdaptive onChange={(e) => setDpr(0.5 + e.factor * 1.5)} />
{/* <YourScene /> */}
</Canvas>
);
}Options
| Prop | Default | Meaning |
| ------------ | ------------------------------------------- | ------------------------------------------------------------ |
| iterations | 10 | Samples collected before each decision |
| threshold | 0.75 | Fraction of samples that must cross a bound to trigger |
| step | 0.1 | Amount added/subtracted from factor |
| factor | 0.5 | Starting factor |
| flipflops | Infinity | Max incline/decline flips before onFallback fires |
| bounds | (hz) => (hz > 100 ? [60, 100] : [40, 60]) | [lower, upper] FPS bounds, given the observed refresh rate |
The refresh rate is auto-detected from the rAF cadence at mount and refined by the highest FPS seen during the session — no configuration needed.
Callbacks
Every callback receives the engine: { fps, factor, refreshrate, gpu, cpu, fallback, … }.
onIncline(e)— sustained FPS above the upper bound: the device has headroom.onDecline(e)— sustained FPS below the lower bound: reduce quality.onChange(e)— the factor changed, in either direction.onFallback(e)— flipped more thanflipflopstimes: performance is unstable, set a fixed low baseline.
GPU-bound or CPU-bound? Each sample carries the real render times (ms), so you can pull the right lever:
<PerfAdaptive
onDecline={(e) => {
if (e.gpu > e.cpu)
lowerResolution(); // GPU-bound → dpr/effects help
else reduceDrawCalls(); // CPU-bound → dpr won't help
}}
/>Hook
Children of <PerfAdaptive> can subscribe with usePerfAdaptive — the equivalent
of drei's usePerformanceMonitor:
usePerfAdaptive({ onChange: (e) => setQuality(e.factor) });The decision core is also exported as a plain class, AdaptiveEngine
(addSample(fps, gpu?, cpu?)), if you want to feed it samples yourself.
Starting quality — useGpuTier()
Runtime adaptation only reacts after the first slow frames. To start at a
sensible quality per device, use useGpuTier() — a suspending wrapper around
@pmndrs/detect-gpu:
import { Suspense } from "react";
import { useGpuTier } from "r3f-monitor";
function Effects() {
const { tier } = useGpuTier(); // 0 (weakest) … 3 (strongest)
return tier >= 2 ? <FancyPostProcessing /> : null;
}
// <Suspense fallback={null}><Effects /></Suspense>useDetectGPU is exported as a drei-compatible alias, and <GpuTier> offers a
render-prop version. The component calling the hook must be under <Suspense>.
One engine, no conflicts
Since v2.2 the measurement core is a ref-counted singleton: mounting
<PerfHeadless /> and <PerfMonitor /> at the same time — or toggling the UI
on and off — always runs exactly one engine. The first mount's options win;
a console warning is logged if a later mount passes different options.
Usage
import { Canvas } from "@react-three/fiber";
import { PerfMonitor } from "r3f-monitor";
function App() {
return (
<Canvas>
<PerfMonitor />
</Canvas>
);
}Maintainers :
Thanks
Special thanks to twitter @utsuboco.
r3f-monitor was originally based on the excellent r3f-perf project and has since evolved with additional metrics, components, UI improvements, and various enhancements.
