Skip to content

Instantly share code, notes, and snippets.

@ultrox
Created February 9, 2026 20:41
Show Gist options
  • Select an option

  • Save ultrox/53116c47795ca0d1d8d2acc838cbabb0 to your computer and use it in GitHub Desktop.

Select an option

Save ultrox/53116c47795ca0d1d8d2acc838cbabb0 to your computer and use it in GitHub Desktop.
Tr
import { useState, useEffect, useRef, useCallback } from "react";
const BUFFER_SIZE = 120;
const TARGET_FPS = 60;
const TARGET_FRAME_MS = 1000 / TARGET_FPS;
function classifyFps(fps) {
if (fps >= 55) return "good";
if (fps >= 30) return "warn";
return "bad";
}
function classifyMs(ms) {
if (ms <= 18) return "good";
if (ms <= 33) return "warn";
return "bad";
}
const COLORS = {
good: "#22c55e",
warn: "#eab308",
bad: "#ef4444",
};
const GRAPH_WIDTH = 280;
const GRAPH_HEIGHT = 80;
const BAR_GAP = 1;
export default function FPSMonitor({ position = "top-right", collapsed: initialCollapsed = false }) {
const [fps, setFps] = useState(60);
const [frameMs, setFrameMs] = useState(16.67);
const [minFps, setMinFps] = useState(60);
const [maxFps, setMaxFps] = useState(60);
const [collapsed, setCollapsed] = useState(initialCollapsed);
const bufferRef = useRef(new Float64Array(BUFFER_SIZE).fill(TARGET_FRAME_MS));
const indexRef = useRef(0);
const lastTimeRef = useRef(performance.now());
const rafRef = useRef(null);
const canvasRef = useRef(null);
const sampleCountRef = useRef(0);
const minFpsRef = useRef(60);
const maxFpsRef = useRef(60);
const drawGraph = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
const w = GRAPH_WIDTH;
const h = GRAPH_HEIGHT;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = `${w}px`;
canvas.style.height = `${h}px`;
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, h);
// Background
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.fillRect(0, 0, w, h);
// Threshold lines
const maxMs = 50; // clip at 50ms (20fps)
// 60fps line (16.67ms)
const y60 = (TARGET_FRAME_MS / maxMs) * h;
ctx.strokeStyle = "rgba(34,197,94,0.3)";
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(0, h - y60);
ctx.lineTo(w, h - y60);
ctx.stroke();
// 30fps line (33.33ms)
const y30 = (33.33 / maxMs) * h;
ctx.strokeStyle = "rgba(239,68,68,0.3)";
ctx.beginPath();
ctx.moveTo(0, h - y30);
ctx.lineTo(w, h - y30);
ctx.stroke();
ctx.setLineDash([]);
// Draw bars
const barWidth = (w - BAR_GAP * (BUFFER_SIZE - 1)) / BUFFER_SIZE;
const buffer = bufferRef.current;
const currentIndex = indexRef.current;
for (let i = 0; i < BUFFER_SIZE; i++) {
// Read from oldest to newest
const idx = (currentIndex + i) % BUFFER_SIZE;
const ms = buffer[idx];
const clampedMs = Math.min(ms, maxMs);
const barH = (clampedMs / maxMs) * h;
const x = i * (barWidth + BAR_GAP);
const classification = classifyMs(ms);
ctx.fillStyle = COLORS[classification];
// Slight transparency for older frames
ctx.globalAlpha = 0.4 + 0.6 * (i / BUFFER_SIZE);
ctx.fillRect(x, h - barH, barWidth, barH);
}
ctx.globalAlpha = 1;
}, []);
useEffect(() => {
let running = true;
const tick = (now) => {
if (!running) return;
const delta = now - lastTimeRef.current;
lastTimeRef.current = now;
// Skip unreasonable deltas (tab was hidden, etc.)
if (delta > 0 && delta < 500) {
const buffer = bufferRef.current;
buffer[indexRef.current] = delta;
indexRef.current = (indexRef.current + 1) % BUFFER_SIZE;
sampleCountRef.current++;
const currentFps = 1000 / delta;
// Update min/max after initial settling (first 60 frames)
if (sampleCountRef.current > 60) {
if (currentFps < minFpsRef.current) minFpsRef.current = currentFps;
if (currentFps > maxFpsRef.current) maxFpsRef.current = currentFps;
}
// Calculate average FPS from last 20 frames
let sum = 0;
const sampleSize = Math.min(20, BUFFER_SIZE);
for (let i = 0; i < sampleSize; i++) {
const idx = (indexRef.current - 1 - i + BUFFER_SIZE) % BUFFER_SIZE;
sum += buffer[idx];
}
const avgMs = sum / sampleSize;
const avgFps = 1000 / avgMs;
setFps(Math.round(avgFps));
setFrameMs(avgMs);
setMinFps(Math.round(minFpsRef.current));
setMaxFps(Math.round(maxFpsRef.current));
if (!collapsed) {
drawGraph();
}
}
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => {
running = false;
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [collapsed, drawGraph]);
const positionStyles = {
"top-right": { top: 12, right: 12 },
"top-left": { top: 12, left: 12 },
"bottom-right": { bottom: 12, right: 12 },
"bottom-left": { bottom: 12, left: 12 },
};
const classification = classifyFps(fps);
const fpsColor = COLORS[classification];
return (
<div
style={{
position: "fixed",
zIndex: 999999,
...positionStyles[position],
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace",
fontSize: 11,
color: "#e2e8f0",
userSelect: "none",
pointerEvents: "auto",
}}
>
<div
style={{
background: "rgba(15, 23, 42, 0.92)",
backdropFilter: "blur(12px)",
border: "1px solid rgba(255,255,255,0.08)",
borderRadius: 10,
overflow: "hidden",
boxShadow: "0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.05)",
minWidth: collapsed ? "auto" : GRAPH_WIDTH + 20,
}}
>
{/* Header */}
<div
onClick={() => setCollapsed((c) => !c)}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "8px 12px",
cursor: "pointer",
borderBottom: collapsed ? "none" : "1px solid rgba(255,255,255,0.06)",
transition: "background 0.15s",
}}
onMouseEnter={(e) => (e.currentTarget.style.background = "rgba(255,255,255,0.04)")}
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{/* Pulsing dot */}
<div
style={{
width: 7,
height: 7,
borderRadius: "50%",
background: fpsColor,
boxShadow: `0 0 6px ${fpsColor}`,
animation: classification === "bad" ? "fpsPulse 0.6s ease-in-out infinite alternate" : "none",
}}
/>
<span style={{ fontWeight: 700, fontSize: 13, color: fpsColor, letterSpacing: "-0.02em" }}>
{fps}
</span>
<span style={{ color: "#94a3b8", fontSize: 10, fontWeight: 500 }}>FPS</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ color: "#64748b", fontSize: 10 }}>{frameMs.toFixed(1)}ms</span>
<span
style={{
color: "#64748b",
fontSize: 9,
transform: collapsed ? "rotate(0deg)" : "rotate(180deg)",
transition: "transform 0.2s",
display: "inline-block",
}}
>
</span>
</div>
</div>
{/* Expanded panel */}
{!collapsed && (
<div style={{ padding: "8px 10px 10px" }}>
{/* Canvas graph */}
<canvas
ref={canvasRef}
style={{
borderRadius: 6,
display: "block",
width: GRAPH_WIDTH,
height: GRAPH_HEIGHT,
}}
/>
{/* Stats row */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: 8,
padding: "0 2px",
}}
>
<StatLabel label="MIN" value={minFps} color={COLORS[classifyFps(minFps)]} />
<StatLabel label="AVG" value={fps} color={fpsColor} />
<StatLabel label="MAX" value={maxFps} color={COLORS[classifyFps(maxFps)]} />
<StatLabel label="FRAME" value={`${frameMs.toFixed(1)}ms`} color="#94a3b8" />
</div>
{/* Legend */}
<div
style={{
display: "flex",
gap: 12,
marginTop: 8,
justifyContent: "center",
}}
>
<Legend color={COLORS.good} label="≥55" />
<Legend color={COLORS.warn} label="30–55" />
<Legend color={COLORS.bad} label="<30" />
<span style={{ color: "#475569", fontSize: 9 }}>fps</span>
</div>
</div>
)}
</div>
{/* Keyframe animation for bad FPS pulsing */}
<style>{`
@keyframes fpsPulse {
from { opacity: 0.5; }
to { opacity: 1; }
}
`}</style>
</div>
);
}
function StatLabel({ label, value, color }) {
return (
<div style={{ textAlign: "center" }}>
<div style={{ fontSize: 9, color: "#475569", fontWeight: 600, letterSpacing: "0.05em" }}>{label}</div>
<div style={{ fontSize: 12, fontWeight: 700, color, marginTop: 1 }}>{value}</div>
</div>
);
}
function Legend({ color, label }) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<div style={{ width: 6, height: 6, borderRadius: 2, background: color }} />
<span style={{ color: "#64748b", fontSize: 9 }}>{label}</span>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment