
This tool works with both Expo and React Native CLI projects. Just install and go.
Record FPS, CPU, memory, and jank while repeating a test on a device. Compare saved runs or benchmark variants under the same conditions. Available metrics depend on your native dependencies and build mode.
Start with one repeatable interaction. Record it before and after a change, then check the UI as well as the metrics.

measure it, in 73 secondsThis interactive Bench demo uses simulated metrics and runs. These figures are not measurements of your browser or device.
Bench relies on native peers. Install the package with Reanimated and Worklets (required), plus the performance toolkit and Nitro modules that power true native UI-thread metrics:
npm install @buoy-gg/perf-monitor react-native-reanimated react-native-worklets react-native-performance-toolkit react-native-nitro-modulesThese ship native code, so rebuild the app with a custom dev build (expo prebuild then npx expo run:ios / run:android) — not Expo Go — and run pod install on iOS. See Reanimated's setup for its Babel plugin. Without react-native-performance-toolkit, Bench still runs in JS-only mode; adding it unlocks native UI FPS and CPU.
Once installed, Bench appears in the floating menu. On Buoy Desktop it also renders as a live HUD you can watch while you use the app.
Bench also runs in the browser — Expo web, Electron, or any React DOM app — with no native modules and no dev build. On web the HUD samples browser APIs instead:
requestAnimationFrame). On web there's a single thread, so JS FPS and UI FPS read the same value.performance.memory (Chromium-based browsers; the row hides elsewhere).pushState). Watch it while you click around to see exactly which pages are slow.React Native Web apps get this automatically. Plain React apps need the standard one-line bundler alias (react-native → react-native-web) and can render the HUD directly:
import { PerfMonitorOverlay, PerfMonitorController } from "@buoy-gg/perf-monitor";
// anywhere in your tree
<PerfMonitorOverlay />
// toggle it
PerfMonitorController.toggle();
import { PerfMonitorOverlay, PerfMonitorController } from "@buoy-gg/perf-monitor";
// anywhere in your tree
<PerfMonitorOverlay />
// toggle it
PerfMonitorController.toggle();
A batch runs one throwaway case before the first real one, and drops it from the results.
The first run can include sheet dismissal, navigation, and initial mounting. The warmup absorbs that setup work so the recorded cases begin under more comparable conditions.
So a batch of four cases records five runs and takes about a fifth longer than the arithmetic suggests. The report shows four.
The sampler runs on the JS thread, so it physically can't take samples while the JS thread is blocked. Instead of silently skipping those moments (which would make stalls invisible in recordings and on the desktop dashboard's live HUD), the monitor detects the gap when sampling resumes and backfills it with reconstructed 0 JS FPS samples — so history sparklines, live sync, and saved reports all show the stall. Reconstructed samples are marked synthetic in report JSON, counted in the report's stats, and never fabricated across app backgrounding. One consequence worth knowing: recordings made before this behavior existed under-report JS stalls, so a stall-heavy run recorded today will (honestly) score worse than the same behavior recorded on an older version.
With @buoy-gg/highlight-updates installed, every recording — manual or batch — also captures which components rendered, how many times, and how long they took (self time, React DevTools profiler convention). Batch reports gain a Top re-renderers section per case, single-run reports get a Render commits block, and the MCP run_benchmark_batch ranking includes the heaviest components per case — so instead of "case B dropped to 41 JS FPS" you get "case B dropped to 41 JS FPS because ProductList rendered 47× costing 312ms".
A few things to know:
captureRenders: false works on run_benchmark_batch.View, Text, Animated(View), Touchable internals, VirtualizedList cells, …) are folded into the run totals instead of cluttering the component list — the rows you see are your components.@buoy-gg/highlight-updates installed in the app. Without it the switches are greyed out and say so.Buoy Optimize is a skill that lets your coding agent drive Bench. Tell the agent which screen is slow and it builds each idea for a fix as its own variant, benchmarks every variant on the device with run_benchmark_batch, and keeps what's faster. You check that each variant still looks right, which matters most on Skia and other GPU-drawn UI. Say "buoy optimize" in your editor to start.

28 lights to 12,000, in 80 secondsSee Buoy Optimize for how a round works, what you need and a worked example.
Install @buoy-gg/perf-monitor and record a run — Bench samples both UI-thread and JS-thread FPS on the device, along with CPU and memory, and saves the run for comparison.
Yes — runs are saved and comparable, and via the Buoy MCP server an AI agent can run benchmark batches of both variants and report which is faster.
Browser measurements use frame timing, available JS heap data, and long tasks. Native CPU, RSS, and thermal measurements remain device-specific. Import it from the package's /web entry (7.0.41 or later). See Web installation for registration, dependencies, and browser boundaries.