docs

JS Top

+
UniversalWorks with Expo & React Native CLI

This tool works with both Expo and React Native CLI projects. Just install and go.

Rank the callback origins JS Top can observe by execution time and call count. Compare timers, animation callbacks, and Promise reactions while reproducing a slow interaction. Work outside the wrapped paths appears as unattributed.

The tool uses JavaScript instrumentation and can run in Expo Go. Production access requires Pro.

what's busy, in 74 seconds

Explore example thread activity

1 / 6

This interactive JS Top demo uses simulated busy percentages and callback timings. It does not profile your device.

9:41
Loading live demo…
Mock thread load, real component — the same code that renders in your app. Step through the tour, or just start tapping.

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 estimated 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.

  • Inactive 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. On a free account it returns the busy % and freezes. The ranked table needs Pro.


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 attributed when they use the wrapped paths.
  • Hermes runs async/await continuations through an internal path that bypasses .then — async function bodies also land in unattributed.

What's Next


FAQ

How do I find what's blocking the JS thread in React Native?

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 appears in the ranking when captured.

Does it need a native module, dev client, or attached debugger?

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.

Why is some time reported as "unattributed"?

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 attributed when they use the wrapped paths.

Web support

Register this package’s /web namespace in FloatingDevTools modules to use its shared panels and actions in a browser app. Import it from the package's /web entry (7.0.41 or later). See Web installation for registration, dependencies, and browser boundaries.