This tool works with both Expo and React Native CLI projects. Just install and go.
Every React Native developer has watched JS FPS drop and asked the same question: what is eating the thread? JS Top answers it live, like Task Manager or Activity Monitor answers it for your computer — a ranked table of task origins (setInterval ← startPolling, Promise.then ← api.ts, requestAnimationFrame ← rafSpinLoop) with time consumed, call counts, and share of thread busy time, updating as you watch.
It's pure JavaScript — no native module, no dev client, no debugger attached. It works in Expo Go and even in release builds.
Watch a forgotten setInterval ← startPolling climb the table, match 50ms+ freezes to the callback that blocked, then Pause the ranking mid-jank:
Not a video — this is the shipped JS Top tool on mock data. Watch thread busy% and the ranked origins update as work hits the JS thread.
npm install @buoy-gg/js-topWith auto-discovery, installing the package is all you need — the JS TOP tool and its toggleable busy-pill HUD appear in your floating menu automatically. Or register it explicitly:
import { FloatingDevTools } from "@buoy-gg/core";
import { jsTopPreset, jsTopModalPreset } from "@buoy-gg/js-top";
<FloatingDevTools apps={[jsTopModalPreset, jsTopPreset]} />
import { FloatingDevTools } from "@buoy-gg/core";
import { jsTopPreset, jsTopModalPreset } from "@buoy-gg/js-top";
<FloatingDevTools apps={[jsTopModalPreset, jsTopPreset]} />
You can't sample a blocked JS thread from JavaScript — so JS Top doesn't sample. Instead it wraps every entry point work can take onto the thread (setTimeout, setInterval, setImmediate, requestAnimationFrame, queueMicrotask, Promise reactions, and legacy-bridge call-ins) and measures each callback precisely, attributing the time to where the callback was scheduled from.
Alongside that, a calibrated high-frequency probe measures true thread occupancy from timer-gap inflation, and React Native's built-in longtask observer flags every 50ms+ block. Anything the wrappers can't see shows up honestly as an unattributed row — the tool never pretends to a coverage it doesn't have.
With the MCP server, an agent can call get_js_thread_top — the device samples for a few seconds and returns the ranked table, so "why is JS FPS low?" becomes a one-tool-call answer.
async/await continuations through an internal path that bypasses .then — async function bodies also land in unattributed.Install @buoy-gg/js-top and open JS TOP — it ranks every task origin (setInterval ← startPolling, Promise.then ← api.ts, requestAnimationFrame ← rafSpinLoop) by the time it consumed, so the runaway timer or callback names itself in seconds.
No. JS Top is pure JavaScript — it wraps the entry points work takes onto the thread instead of sampling, so it runs in Expo Go, dev builds, and release builds with no debugger attached.
On the New Architecture, touch handlers and React commit work enter the JS thread through paths pure JavaScript can't wrap. That time is reported honestly as unattributed rather than being blamed on the wrong origin. Timers, rAF, microtasks, and Promise chains are always fully attributed.