Three.js From Zero · Article s8-09
S8-09 Telemetry & Perf Monitoring
Season 8 · Article 09
Telemetry & Performance Monitoring
Your dev machine gets 60fps. Your user's Chromebook gets 24. You'd never know without telemetry. Core Web Vitals for 3D, frame budget tracking, device-tier heuristics.
1. What to measure
| Metric | Target | Why |
|---|---|---|
| LCP (3D ready) | < 2.5s | Google ranking signal, UX |
| FPS p50 | > 45 | Smooth feels good |
| FPS p10 | > 30 | Slowest users still functional |
| Long tasks (> 50ms) | < 1/session | Responsiveness |
| GPU memory | < 500MB | Mobile crash avoidance |
| Error rate | < 0.5% | Regression detection |
2. Core Web Vitals (web-vitals library)
import { onLCP, onCLS, onINP } from 'web-vitals';
onLCP(m => send('lcp', m.value));
onCLS(m => send('cls', m.value));
onINP(m => send('inp', m.value));
3. Custom "3D ready" metric
const t0 = performance.now();
loader.load(url, gltf => {
scene.add(gltf.scene);
// Wait for first render
requestAnimationFrame(() => requestAnimationFrame(() => {
const dt = performance.now() - t0;
send('3d-ready', dt);
}));
});
4. FPS tracking
const samples = [];
let last = performance.now();
function tick() {
const now = performance.now();
samples.push(1000 / (now - last));
last = now;
if (samples.length > 300) samples.shift();
requestAnimationFrame(tick);
}
// Every 30s, report
setInterval(() => {
samples.sort((a,b) => a-b);
send('fps-p10', samples[samples.length * 0.1 | 0]);
send('fps-p50', samples[samples.length * 0.5 | 0]);
send('fps-p90', samples[samples.length * 0.9 | 0]);
}, 30000);
5. Long task detection
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
send('long-task', { duration: entry.duration, name: entry.name });
}
}
}).observe({ entryTypes: ['longtask'] });
6. GPU memory
// Rough — from renderer.info
setInterval(() => {
send('gpu-mem', {
geometries: renderer.info.memory.geometries,
textures: renderer.info.memory.textures,
});
}, 60000);
True GPU memory isn't exposed. Geometry/texture counts are a proxy.
7. Device tier classification
function detectTier() {
const gpu = renderer.getContext().getParameter(
renderer.getContext().RENDERER
);
if (/RTX|M1|M2|Apple GPU/.test(gpu)) return 'high';
if (/GTX|Intel Iris|Mali-G/.test(gpu)) return 'mid';
return 'low';
}
// Send with every metric so you can segment
send('metric', { tier: detectTier(), ... });
8. Send to backend
function send(event, data) {
if (navigator.sendBeacon) {
navigator.sendBeacon('/telemetry',
JSON.stringify({ event, data, ts: Date.now() }));
} else {
fetch('/telemetry', { method: 'POST', body: JSON.stringify({ event, data }) });
}
}
sendBeacon survives page unload. Regular fetch loses data on navigation.
9. Live demo — in-app perf panel
–
10. Dashboards
Ship raw events → query with:
- Grafana + Prometheus (self-host).
- Datadog / New Relic (hosted).
- Vercel Analytics (built into Next).
- Plausible / Fathom (privacy-friendly).
Track each metric over time. Alert on p10 regression.
11. Takeaways
- LCP, CLS, INP via web-vitals library.
- Custom "3D ready" after first render.
- FPS percentiles (not just average).
- Long Task Observer for responsiveness.
- Device-tier classification for segmentation.
- sendBeacon so unloads don't lose data.