# Bench

<!-- ::platform-badge platform="both" -->

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.

<!-- ::tool-film id="bench" -->

<!-- ::perf-monitor-live-demo -->

## Installation

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:

<!-- ::PM npm="npm install @buoy-gg/perf-monitor react-native-reanimated react-native-worklets react-native-performance-toolkit react-native-nitro-modules" yarn="yarn add @buoy-gg/perf-monitor react-native-reanimated react-native-worklets react-native-performance-toolkit react-native-nitro-modules" pnpm="pnpm add @buoy-gg/perf-monitor react-native-reanimated react-native-worklets react-native-performance-toolkit react-native-nitro-modules" bun="bun add @buoy-gg/perf-monitor react-native-reanimated react-native-worklets react-native-performance-toolkit react-native-nitro-modules" -->

These 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](https://docs.swmansion.com/react-native-reanimated/) 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](../desktop) it also renders as a live HUD you can watch while you use the app.

### Web

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:

- **FPS** — main-thread frame rate (`requestAnimationFrame`). On web there's a single thread, so JS FPS and UI FPS read the same value.
- **BUSY** — replaces the CPU row: the % of recent time the main thread was blocked by [long tasks](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongTaskTiming) (>50ms). This is the "why does it feel janky" number.
- **JS heap** — `performance.memory` (Chromium-based browsers; the row hides elsewhere).
- **PAGES** — the full HUD ranks your slowest routes by average FPS while each was active (browser-history route tracking — works with React Router, Next, hash routers, or plain `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:

```tsx
import { PerfMonitorOverlay, PerfMonitorController } from "@buoy-gg/perf-monitor";

// anywhere in your tree
<PerfMonitorOverlay />
// toggle it
PerfMonitorController.toggle();
```

---

## What You Can Do

- **Live HUD** — Watch UI FPS, JS FPS, CPU, and memory update in real time as you navigate.
- **Record runs** — Capture a session, save it, and keep a library of recordings.
- **Compare** — Long-press to select two or more saved runs and see them side-by-side in the same bar-chart report batches get (duration, memory, JS FPS, CPU) to prove a change made things faster.
- **Batch benchmarks** — Run the same flow across several variants and get a ranked report with per-metric leaders and at-risk flags.
- **Render capture** — See *why* a run was slow: every recording also captures per-component render counts and durations (React-profiler-style), so reports show the top re-rendering components next to the FPS numbers.

### The warmup case

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.

### Stall reconstruction

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.

---

## Render Capture

With [`@buoy-gg/highlight-updates`](./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:

- Requires a **dev build** (the React DevTools hook and profiling timers aren't present in release builds). Without them the run simply has no render data — nothing breaks.
- Capture walks committed fibers on the JS thread, adding a small overhead per commit. It's applied **uniformly to every case** in a batch, so relative comparisons stay fair — but for absolute FPS measurements you can turn it off: the **Capture render commits** toggle in the tool's Settings covers manual recordings, the **Capture renders** toggle in Automate settings covers batches, and `captureRenders: false` works on `run_benchmark_batch`.
- Buoy's own devtools UI (the HUD, floating menu, etc.) is excluded from results automatically, and React Native framework wrappers (`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.
- Want to *watch* the renders as they happen? Turn on **Show render highlights while recording** in the tool's Settings (off by default). While a recording is running, the [Render Highlighter](./highlight-updates)'s bounding boxes flash in real time, then the overlay returns to whatever state it was in. Outside a recording this setting draws nothing; use the Highlight Updates tool in the menu for that. The boxes themselves cost real frame time, so leave this off when you care about the numbers.
- Both render settings need `@buoy-gg/highlight-updates` installed in the app. Without it the switches are greyed out and say so.

---

## Optimize with your AI

[Buoy Optimize](../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.

<!-- ::tool-film id="optimize" -->

See [Buoy Optimize](../optimize) for how a round works, what you need and a worked example.

---

## What's Next

- [Buoy Optimize](../optimize) — Let your AI agent benchmark and pick fixes
- [AI / MCP Server](../mcp) — Automate benchmarks with an AI agent
- [Render Highlighter](./highlight-updates) — Find the re-renders hurting performance
- [Events Timeline](./events) — See performance in context with everything else

---

## FAQ

### How do I measure FPS in a React Native app?

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.

### Can I compare performance before and after a change?

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.

## Web support

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](../web/installation#tool-setup) for registration, dependencies, and browser boundaries.
