An AI Agent for Measured React Native Performance Fixes

Authors
Xavier Costa
Frontend Engineer
@
Uphold

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 in the React Native Best Practices announcement. 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:

Old code New code
Runs (ms) 1240, 1220, 1260, 1235, 1228 1040, 1055, 1038, 1062, 1048
Average 1237 ms 1049 ms
Improvement 188 ms
Bar to clear ~30 ms
Verdict KEEP - miles past the bar

REVERT sprinkling useMemo everywhere:

Old code New code
Runs (ms) 1240, 1220, 1260, 1235, 1228 1218, 1245, 1226, 1252, 1219
Average 1237 ms 1232 ms
Improvement ~5 ms
Bar to clear ~30 ms
Verdict REVERT - 5 ms lost in 34 ms of wobble

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:

  1. runs anti-pattern detectors based on /react-native-best-practices,
  2. 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:

  1. The faulty component every screen imports.
  2. The exports that could be more modular.
  3. 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:

  1. scans the repo,
  2. picks the components with the worst performance debts,
  3. measures and implements fixes,
  4. 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 metrognome

Then /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.

Insights

Learn more about AI

Building a Software Factory: Write Specs an Agent Can Build From — cover

Nov 6Article

Building a Software Factory: Write Specs an Agent Can Build From

Learn how to research requirements, resolve open decisions, and write testable specs that AI coding agents can implement without the planning conversation.

 Large-Scale Migrations With Coding Agents: Planning, Verification, and Rollback — cover

Nov 4Article

Large-Scale Migrations With Coding Agents: Planning, Verification, and Rollback

Plan codebase migrations with coding agents using reviewable requirements, verification checks, staged rollouts, and defined rollback criteria.

Conversational Banking: From Customer Questions to Authorized Actions — cover

Nov 2Article

Conversational Banking: From Customer Questions to Authorized Actions

Explore conversational banking architecture through a card-dispute example, covering tool access, authorization, execution, observability, and human handoff.

Building a Software Factory: The Night My Agents Waited for Me — cover

Oct 28Article

Building a Software Factory: The Night My Agents Waited for Me

Learn how permissions, review limits, and missing product decisions stalled two overnight coding agent runs, and how their maintainer revised the workflow.

Your AI Coding Agent Still Needs an Architecture — cover

Oct 22Article

Your AI Coding Agent Still Needs an Architecture

Use explicit contracts, behavior tests, and dependency checks to guide AI coding agents and review their changes through a TypeScript favorites example.

cover

Oct 15Live Stream

Who Left the Agents Running? React Native Software Factories

Join Kewin Wereszczyński and Szymon Chmal to explore what can go wrong in a React Native software factory overnight and the controls that keep agents on track.

cover

Oct 14Podcast

When Coding Gets Faster, What Breaks Next?

Matt Hargett explains how AI moves bottlenecks across testing, deployment, infrastructure, support, and the rest of software delivery.

cover

Oct 12Podcast

AI Coding Agents and Hermes With Tzvetan Mikov

Tzvetan Mikov discusses AI coding agents, context switching, low-level engine optimization, reviewing specs and tests, and his plans for Hermes X.

cover

Oct 12Podcast

State Machines for AI Agents With David Khourshid

David Khourshid explains state machines for AI agents, human approval steps, tracing, and handling invalid model outputs in agent workflows.

cover

Oct 9Article

How to Maximize AI SDLC? Measure the Outer Business Loop, Not the Inner Developer Loop

Connect AI-assisted development to measurable business outcomes. Matt Hargett shares a checklist for assessing user exposure, results, and delivery costs.

cover

Oct 7Talk

Automating Mobile QA with Cloud Agents

Michał Pierzchała shows how cloud-based QA agents can test real iOS and Android user flows and bring screenshots, recordings, and logs into pull requests.

cover

Oct 1Live Stream

Can Jev Make AI Agent Decisions Faster?

Explore Jev with Kewin Wereszczyński and Szymon Chmal: how it works, its approach to faster agent decisions, and possible local alternatives.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.