An AI Agent for Measured React Native Performance Fixes
Last Tuesday, I went to bed angry at a scroll view. For two hours, I'd been chasing 60fps and getting nowhere I could trust. I'd make a change, the numbers looked a little better, but I never knew whether I’d actually improved anything or just gotten a luckier run.
Before I closed the laptop, I ran one command on Claude Code, described the problem, and went to sleep.
By morning, it had shipped six commits, each carrying its measured FPS gain and a one-line description of the fix. It had also tried and thrown out two other fixes, because they didn't clear the bar. The diff was 200 lines; every hypothesis was logged in memory.
I did nothing. I was asleep.
The two discarded iterations are the intriguing part. Any tool can propose a change and commit it; the real work is knowing which ones to throw away, through repeated efficient measurement and without any human supervision. That’s what this article is about.

"Feels faster" isn't proof
"Feels faster" is the most expensive sentence in modern mobile performance engineering. It's also unfalsifiable, which is exactly why we keep saying it.
Here's a normal React Native performance session: You make a change, open the app, scroll, and record a Profile trace. You compare it to your baseline, see a lower number, and think, "nice." But was it actually your change, or just measurement noise? You tested it once. The phone was warm from your last try, the account data was already cached, and nothing else was fighting for the CPU. You measured a mood, not a change.
Your ego still skyrockets. You wrote the cleanest code you can recall from your entire Software Engineering career, the improvement showed up on your Profiler, so you commit and move on, quietly convinced your manager should pioneer a new Performance department inside your company with you as the lead. Then the real users arrive: different devices, worse networks, colder starts. Three weeks later, your beloved Product Manager files a new ticket: "Performance still janky". So what was all your flawless code for?
Callstack's DMAIC-based work with Expensify leaned on repeated, statistical measurement for exactly this reason: a single sample can tell you nothing, or worse, something wrong. The only cure is to take enough samples that the noise averages out, then gate the result strictly so the tool can tell itself no.
Measuring was never the problem. Every profiler hands you a number - whether you wrote clean code or not. The question is: is that number real or is it about to add entropy to your repository? Repeated measurement is what tells the two apart.
Anatomy of an autonomous performance engineer
So what actually made that overnight loop trustworthy?
Meet Metrognome. Not another profiler or benchmark, but an autonomous React Native performance engineer that proposes, measures, validates, and rejects its own work.
It's an AI plugin that turns a scattered RN performance workflow into a single repeating loop, borrowed from Andrej Karpathy's /autoresearch and introduced to React Native: for a given problem, propose a hypothesis (sketch of a fix/optimisation), run the experiment, measure it N times on a live app, keep it only if it beats the noise, commit it with the evidence. Then pick the next hypothesis and go again, one variable at a time, only stopping when the goal is achieved.
It doesn't ship its own profiler, its own device driver, or its own opinion about good RN code. It brings together the best tools that already exist, most of them Callstack's, and only orchestrates the research with its own loop and memory. If you drew Metrognome as a body, almost every organ would carry a Callstack logo. And that's not a marketing line, it's literally how the code works. It means the senses and the instincts are already battle-tested, and Metrognome just makes sure the system keeps flowing.

Metrognome connects several existing tools into one performance loop, and each one covers a distinct part of the workflow:
- Eyes, hands and nerves: Agent Device, which drives the app: reads the screen from an accessibility snapshot, taps, scrolls, navigates, opens and closes screens to drive each measurement. It also runs Agent React DevTools inside it, so it feels every re-render and names the component that fired, this way "did memoizing this actually cut renders?" gets an answer, not a guess. Agent Device is the most crucial integration; without it, Metrognome would be cooking a recipe blindfolded.
- Pulse: Metro MCP, which reads the Hermes heartbeat over CDP: CPU, JS heap, network. The raw signal the gate runs on.
- Brain: React Native Best Practices, which you can read more about here. Facing any complex performance dilemma, Metrognome doesn't improvise a fix; it reads the playbook Callstack's team already wrote.
- The orchestrator: Metrognome. It’s the loop, the gate, the memory, and it synchronizes the symphony while not playing any instrument.
The gate: how it refuses to fool itself
The gate's whole job is to disappoint you. A 7 ms "win" hiding inside a 20 ms outlier isn't a win; it's the device having a good day. Metrognome reverts these cases.
The mechanism is deliberately not fancy. For every experiment, it runs the scenario N times. The first run is always a warm-up: it initializes caches and other startup state, so it is excluded from the average. The remaining runs are averaged, for both the old code and the new.
An average alone still isn't enough, because no device is perfectly steady. Build the exact same code, measure it twice, and the two numbers won't match. That gap, the run-to-run wobble, is what fools developers. Metrognome keeps a change only if it clears two points at once:
- It beats a floor an user would actually feel. Nobody notices 5 ms. Set a floor, say 30 ms on cold start, and anything under it is noise by definition and discarded by Metrognome.
- It beats the wobble by a comfortable margin. If subsequent runs for the same screen differ by ±15 ms, a real win has to be about 30 ms or more before it's trusted.
All these values and floors are customizable to fit your own preferences and project needs. Here’s an example of one change that passes and one that doesn't:
KEEP swapping FlatList for FlashList:
REVERT sprinkling useMemo everywhere:
The second one is particularly important, because the new average is lower. A naive tool would commit it, call it a win, and quietly increase memory usage for no measurable benefit. Metrognome throws it out because a 5 ms gap on a screen that swings by 30-plus ms could just as easily flip the other way next run.
How Uphold actually uses it
For a crypto fintech, responsive transaction flows help users stay oriented as they move money through the app. Uphold has used Metrognome to identify performance issues in its mobile app, including the following two examples:
- High-level state: A slice of global state lived at the dashboard root but was only ever read by a bottom sheet component that opened very occasionally. A background poll refreshed it every five seconds, silently re-rendering the entire dashboard while the sheet sat closed. It had been there for years. Moving the state down to the sheet stopped the every-5s re-render storm cold.
- An array spread that kills memo: The root context provider's value was already wrapped in
useMemo, so it looked textbook-correct. But one field spread a fresh array on every render,wallets: [...fiat, ...crypto], so the memo's output changed reference anyway and every screen below it re-rendered. The fix wasn't "add a memo"; it was killing that one spread. A one-line villain behind a memo that looked right.
Accepted changes land in a PR as separate commits, each recording the before, after, and verdict in its message. Every experiment, kept or reverted, is also written to a Metrognome's Performance Memory file committed to the app’s repository: one durable line per finding, for example "Memoizing the <Footer /> component didn't beat the wobble" or "FlashList cut this feed's TTI by ~180 ms."
That file is a shared second brain within any repository. When the next Metrognome run proposes a fix the team already disproved, the memory says so before a single measurement runs. Nobody iterates over a dead end, and every experiment anyone runs makes the whole team's next one cheaper. The knowledge compounds; that's the entire goal behind Metrognome: to foster an ever-growing secondary RN performance brain within every project.
Where does the debt live?
Every run so far started with a human pointing at a screen/component, I told it the scroll view hurt. But measurement is the costly half of the loop: N runs per hypothesis, on a live device. Aim for every component in the codebase, over and over, and you'd burn a week’s worth of tokens in a day. Before it measures anything, Metrognome needs a cheaper answer to one question: where does the debt actually live?
See the entropy
That's what the Performance Map is for. Similar to the renowned Graphify skill, Metrognome walks the codebase statically and builds a ranked picture of where the performance debt actually lives, so the autonomous loop spends its nights on the ten components that matter, not the thousand that don't.
You don't need a device or even the plugin to see it. Here's Bluesky's actual codebase as a 3D debt map:
It's a static Babel scan that walks the import graph, and for each React Native component, it:
- runs anti-pattern detectors based on
/react-native-best-practices, - sizes each node by weighted debt and place it by how many other files depend on it.
The bigger and redder the node is, the more performance debt it retains.
More than a linter, the analysis surfaces structural performance debt, including:
- The faulty component every screen imports.
- The exports that could be more modular.
- The excessive memoization on simple string concatenations.
Therefore, a lone StyleSheet.create in a leaf file is not Metrognome's responsibility, whereas a heavily-depended-on component with real problems is.
Simulate your own app's 3D React Native performance map, and let Metrognome quickly identify the entropy that's been hiding in plain sight for years: a bloated design-system component, a utility hook that’s triggering unnecessary renders throughout the app, or a provider whose updates ripple through your navigation tree.
The graph isn't for visualisation only; it serves prioritisation: worst debt, first in line. And once the tool can pick its own targets, why wait for a human at all? Spotting a problem yourself works great, but the graph lets Metrognome go looking without being prompted.
Devs ship features, Metrognome handles the app's performance
The overnight run we started from still had a laptop and a person who went to bed; the guardian agent needs neither, just a schedule. Sounds complex to wire up? It isn't, because Metrognome ships two prebuilt GitHub Actions templates, one device-free and one with an emulator (all five presets, FPS included). On a cron schedule, each one:
- scans the repo,
- picks the components with the worst performance debts,
- measures and implements fixes,
- opens a PR with the gains table and why each change was chosen.
No PR appears unless a fix actually cleared the bar.
This is Continuous Integration in its truest form. The team keeps doing what it's best at, shipping features, while performance is fully handled by an expert right arm. The team writes the product, and Metrognome watches the vitals and keeps your application fully performant. Your product manager thanks you: no more delays to the feature backlog due to performance tickets.
Should you try it?
If you've got a React Native or Expo app, one screen you'd call slow, and you're looking for your next AI automation, point Metrognome at that screen tonight and read the diff in the morning.
You don’t have to trust it, every commit carries repeated validation, along with the measurements that earned it. Review the evidence and merge only if it clears your bar.
/plugin marketplace add uphold/metrognome
/plugin install metrognomeThen /metrognome, pick a preset, and go to bed, or leave it running on the sideline.
Metrognome is open-source: the detectors, the gate, and the presets are all right there to read, argue with, or extend. Innovation is always welcome, be it about more detectors, richer presets, iOS on CI, or a deeper playbook. Issues and pull requests are open, eager for any community support.
If you’d like to learn more about the tool and see how it works in practice, watch this recording from React Native Porto event.
This was a guest post by Xavier Costa at Uphold.
Learn more about AI
Here's everything we published recently on this topic.























