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

MetricTargetWhy
LCP (3D ready)< 2.5sGoogle ranking signal, UX
FPS p50> 45Smooth feels good
FPS p10> 30Slowest users still functional
Long tasks (> 50ms)< 1/sessionResponsiveness
GPU memory< 500MBMobile 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.