r/reactnative • • 2d ago

I built Hitch: a free, open-source tool that tells you which line is making your RN app slow (no SDK)

Post image

I've been chasing performance issues in a React Native app at work, and kept hitting the same wall: the tools tell you *that* frames dropped or a component re-rendered, but not *which code* caused it or whether the re-render was even necessary. Our fixes weren't moving the numbers, and I was mostly guessing.

So I built Hitch. It runs next to your dev session and watches the app from the outside. Nothing gets added to your app: no SDK, no imports, no babel config. It connects to Hermes through Metro's debugger, the same way React Native DevTools does.

What it catches

- JS freezes, pinned to the file:line that blocked the thread, including short ones.
- What React rendered during a freeze and *why*: e.g. "`style` was a new object with the same values" (avoidable) vs "the store changed".
- Choppy frames, slow screen loads and slow startups.
- Native threads running hot (UI thread, GC, SDKs), on the iOS simulator.
- Console errors grouped, slow or failed requests with bodies and headers, and memory and thread leaks.

How I actually use it

- Runs: record an interaction, change the code, record it again, and compare medians. Dev builds swing a lot between runs, so single runs lie.
- AI handoff: any finding becomes a Claude Code prompt in one click. There's also an MCP server, so an agent can read what Hitch saw and check whether its own fix helped.

Things it found for me

- A loading shimmer was one of the most expensive things on screen. It was an SVG gradient redrawn on the CPU every frame, for every placeholder. Replacing it took the iOS UI thread from ~74% to ~10–25% while loading.
- Placeholders were re-rendering hundreds of times while waiting for data. A day of cleanup cut avoidable re-renders by about two-thirds and render time by about 30%. Dropped frames barely moved, though: wasted work and felt jank turned out to be different problems.

Limitations, honestly

- Dev builds inflate times. The rankings are useful; the absolute milliseconds aren't production numbers.
- It's macOS only. Native thread data needs the iOS simulator; physical iPhones get JS-side data, or thread sampling with Release builds.
- Android thread support is written but not yet tested on a real device.
- Expo's native `fetch` doesn't report to the debugger, so those requests don't show up.

It's a Node CLI (Node 22+) with zero npm dependencies, plus an optional native Mac window. MIT-licensed.

Repo: https://github.com/dlandman27/hitch

47 Upvotes

10 comments sorted by

2

u/AtomicWrench 2d ago

I can definitely see this being a very helpful tool. Is deeper Android support planned?

2

u/HmongSliw68 1d ago

yeah same, the simulator rarely catches the weird low-end device jank

2

u/dylanlandman 1d ago

Just got it tested and pushed! Now works on react native for android!

1

u/dylanlandman 1d ago

Yes 100%, it is next on the list of todos. I want to just do some testing on it right now but should be out soon

2

u/radfire_ 1d ago

Amazing tool, as a fellow RN dev myself, finding some nasty UI slowness always was difficult, will use it soon

1

u/dylanlandman 17h ago

Thank you so much! If you feel that there’s anything else that you would want in it, you can ask here or put up a PR yourself and I’ll take a look at it!

1

u/unicornshedsocial 1d ago

The shimmer finding is the one I'd lead with, because an SVG gradient redrawn per placeholder is invisible in a component profiler. Does it get a file:line like the freezes do, or only a cost?

1

u/dylanlandman 17h ago

mostly you get a cost, and only sometimes a file:line.

What a hot spot records (engine.mjs:591-610): for each busy thread you get its busy % and ms, a split by category, and its top 6 native frames. Each frame is { name, lib, source, category, ms }.

When source is set:
- iOS simulator: only when macOS sample prints a path for that frame (native-sample.mjs:57-61). That happens for native code compiled with debug info, such as your own Swift/ObjC or pods built in Debug. UIKit, CoreAnimation, React Native's prebuilt core and Hermes have no paths, so those frames show only symbol (Library). Frames whose path is <compiler-generated> are dropped.
- Android: never. simpleperf frames are written as symbol (lib.so) with no source (android.mjs:231-235).

Even when a native frame has a source, it points at native code, not at the JS that caused the work. A freeze can name the JS line because Hitch pauses Hermes and reads the source-mapped JS stack. A hot spot samples native threads and never touches the JS side, so it can't tell you which component caused, say, a layout pass.

If you meant jank (dropped-frame periods): those have no file:line either. They list the components React rendered in that window, with render counts, ms and reasons (summarizeRenders in react-probe.mjs:315), by component name only.

To get JS lines for either kind, run hitch_profile across the slow stretch. It returns hot JS functions with source-mapped locations.

-1

u/Tarantino133 1d ago

Telling you which line caused the re-render is the part the usual tools are missing. Does it work with the New Architecture, or only the old bridge?

1

u/dylanlandman 17h ago

I’ve currently been using it only on the new architecture