mediumEngineering Practices#81

React Native performance monitor

Prompt

Create a PerformanceMonitor component that shows JS-thread FPS by counting requestAnimationFrame callbacks and sampling with setInterval. Update every second.

Solution

export default function PerformanceMonitor() {
  const [fps, setFps] = useState(0)
  const frameCount = useRef(0)
  useEffect(() => {
    let rafId
    const frame = () => {
      frameCount.current++
      rafId = requestAnimationFrame(frame)
    }
    rafId = requestAnimationFrame(frame)
    const interval = setInterval(() => {
      setFps(frameCount.current)
      frameCount.current = 0
    }, 1000)
    return () => {
      clearInterval(interval)
      cancelAnimationFrame(rafId)
    }
  }, [])
  return <Text style={{ fontSize: 10 }}>{fps} FPS</Text>
}
Mentor's take

The technique is honest and the caveats are the interview. Counting requestAnimationFrame callbacks per second measures the JS thread: if your JS FPS drops, something is blocking the event loop — a heavy render pass, JSON parsing, an accidental O(n²). But React Native runs UI on a separate thread, so JS FPS and UI FPS diverge — a native-driven animation (useNativeDriver, Reanimated worklets) stays at 60 while JS is frozen, and vice versa. Reporting one number as "the FPS" is the junior mistake; know which thread you measured. The in-app Perf Monitor (dev menu) shows both for exactly this reason.

Implementation details that carry the points: the counter lives in a useRef because incrementing it 60 times a second in useState would itself cause 60 renders a second — the monitor becoming the perf problem. The cleanup returns from useEffect and cancels both the interval and the rAF loop; leaking a rAF loop after unmount is a real memory-and-CPU leak.

Also say where this fits: an overlay like this is a dev tool. Production performance is measured as trends — cold start, TTI, slow/frozen frame rates via performance monitoring — not by shipping an FPS counter.

Red flag: measuring in a dev build and quoting the numbers — dev mode disables optimizations and runs dramatically slower; performance claims come from release builds on mid-range hardware.

Say it: "rAF counting measures the JS thread only — UI runs on its own thread in React Native — so I always name which thread dropped frames, and I measure on release builds."