JS Top

+
UniversalWorks with Expo & React Native CLI

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.

Installation

npm install @buoy-gg/js-top

With 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:

tsx
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]} />

How it works

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.

  • Zero overhead when closed. The engine only runs while the tool is open (or a desktop dashboard is watching).
  • Exclusive-time accounting. Nested callbacks never double-count; totals always add up.
  • Blocking-task attribution. Each 50ms+ stall is matched to the callback that overlapped it.

What You Can Do

  • Rank live JS-thread cost — Sort by recent ms, calls, average, max, or total.
  • Catch the runaway interval — A polling loop someone forgot shows up in seconds, named after the function that scheduled it.
  • See thread busy% — A live meter plus 30s history sparkline, honest even through multi-second stalls.
  • Read blocking tasks — Every 50ms+ freeze, with the worst-offending origin named.
  • Watch from the desktop — The Buoy Desktop panel mirrors the device table live, and adds a dev-only per-function Hermes sampler for function-level flame data.

Ask your AI what's eating the thread

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.


Coverage notes

  • On the New Architecture (bridgeless), touch handlers and React commit work enter the thread through paths pure JS can't wrap — that time appears as unattributed (the banner in the tool explains this). Timers, rAF, microtasks, and Promise chains are always fully attributed.
  • Hermes runs async/await continuations through an internal path that bypasses .then — async function bodies also land in unattributed.

What's Next