In React Native's Perf Monitor overlay, what do the separate JS and UI frame rates tell you, and how do you tell which thread janked?
answer
- two threads, two frame rates
- JS drop: taps lag, JS animations freeze
- UI drop: scrolling or native transition stutters
- Android New Architecture overlay omits JS fps
- ScrollView keeps scrolling while JS is blocked
basics
~20 sThe UI rate measures the native main thread that draws views and runs scrolling; the JS rate measures the thread running React and your handlers. Whichever falls, and which symptoms appear, tells you where to look.
solid answer
~50 sReact Native renders on two threads that can drop frames independently, and the Perf Monitor (toggled from the Dev Menu) reports them separately. A **JS frame-rate** drop means the JavaScript thread was busy: taps respond late, `setState`-driven UI freezes and JS-driven animations stall, yet an already-rendered `ScrollView` still scrolls and a `native-stack` transition still animates. A **UI frame-rate** drop means the main thread could not draw in time: scrolling and native transitions themselves stutter even while JS is idle - think overlapping translucent views, animating an `Image`'s width and height, or an oversized view hierarchy. One caveat on current releases: on Android with the New Architecture the default overlay prints UI fps, dropped frames and 4+ frame stutters but **no JS line**, so on Android you infer JS jank from the symptoms or a profile.
go deeper
Know how to open the Perf Monitor from the Dev Menu and that it separates the UI thread's frame rate from the JS thread's.
Map symptoms to threads: late taps and frozen JS animations mean JS, stuttering scroll or native transitions mean UI. Know the Android overlay drops the JS line on the New Architecture.
Show a repeatable routine: release-like build, real device, the scroll test to split threads, then a profiler on the guilty thread before changing code.
Talk about making thread-level jank visible to the team, for example agreeing which interactions must survive a busy JS thread and checking them on low-end devices.
## Two threads, two frame rates A React Native app does its work on more than one thread, and each has to finish its share of a frame within the **frame budget** (about 16.7 ms at 60 Hz, 8.3 ms at 120 Hz): - **JS thread** - runs React rendering, effects, event handlers, network callbacks and any animation computed in JavaScript. - **UI (main) thread** - owns the native views: drawing, native scrolling, native navigation transitions and animations that run natively. Because they are separate, one can be healthy while the other drops frames. That is why the React Native docs point you at **two different frame rates** when you open the Dev Menu and toggle the **Perf Monitor**. ## What the overlay shows on each platform The overlay is a development tool, and its content differs by platform on current releases: | Platform | What the default overlay reports | |---|---| | iOS | a RAM figure and a few older counters, plus small **UI** and **JS** frame-rate graphs | | Android, New Architecture | **UI fps**, frames dropped so far, and stutters of 4+ frames; **no JS fps line** | The Android JS line was tied to a bridge-era idle listener and is only printed on the legacy architecture. Since the New Architecture is the only architecture from 0.82 on, Android readers must diagnose JS-thread jank from symptoms or a profiler rather than from a number in the overlay. On Android the overlay also needs the "display over other apps" permission, which the Dev Menu requests if it has not been granted. ## Reading the symptoms The fastest diagnosis combines the overlay with what the user actually sees: | Symptom | Likely thread | Why | |---|---|---| | Tap feedback arrives late, buttons feel dead | JS | touches must be processed by JS before state changes | | JS-driven `Animated` animation freezes | JS | each step is computed on the JS thread | | Scroll stays smooth while the screen ignores taps | JS | native scrolling does not need JS to move | | Scrolling itself stutters | UI | the main thread cannot draw each frame in time | | A `native-stack` transition stutters | UI | the transition animates on the main thread | | Screen content appears late after a smooth transition | JS | the transition ran natively but JS had not rendered | Typical **JS-thread** causes are expensive re-renders, large synchronous loops such as parsing or sorting big arrays, and heavy work inside event handlers. Typical **UI-thread** causes named by the React Native docs include moving views that need alpha compositing on Android (for instance translucent text over an image), and animating an `Image`'s `width` or `height`, which on iOS re-crops and rescales the source on every frame - animating `transform: [{scale}]` instead avoids that. ## A diagnostic routine 1. Reproduce on a **release-like build** on a real device, since development mode slows the JS thread on its own. 2. Show the Perf Monitor and perform the janky interaction several times. 3. Note which rate drops. On Android, pair the UI number with the symptom table to decide whether JS is the culprit. 4. Test the split directly: while the jank happens, try scrolling a list. If scrolling stays smooth while taps stall, the UI thread is fine and the JS thread is the bottleneck. 5. Move to a profiler for the thread you identified, rather than guessing at optimisations. ## Pitfalls - **Reading the UI number as "the app's fps".** A healthy UI rate does not rule out a frozen JS thread; on Android it is the only number shown. - **Blaming JS for every stutter.** Scroll jank with idle JS points at drawing cost on the main thread. - **Trusting the counts on a 120 Hz Android phone.** The Android overlay computes dropped frames and stutters against a 60 fps target, so on a faster display read the fps figure against the panel's actual rate. - **Diagnosing in debug mode only.** Development-mode overhead exaggerates JS-thread drops. - **Treating the overlay as a profiler.** It tells you which thread is struggling, not which function; that is a job for a trace. The mental model to leave with: two threads share one deadline, the overlay and the symptoms tell you which one missed it, and the fix depends entirely on that answer.
- On Android with the New Architecture, how do you confirm JS-thread jank without a JS fps line?Use behaviour: while the jank happens, scroll an already-rendered list. If scrolling stays smooth but taps and JS-driven animations stall, the UI thread is fine and the JS thread is busy. Then capture a JavaScript profile to find the long task.
- Why can a native-stack push look smooth while the new screen appears blank for a moment?`@react-navigation/native-stack` runs its transition on the main thread, so it keeps animating while the JS thread is busy. The screen's contents, however, need JS to render, so a blocked JS thread shows up as late or empty content rather than a stuttering slide.
- Why can the Android overlay under-report drops on a 120 Hz phone?Its dropped-frame and 4+ stutter counts are computed against a 60 fps target. On a panel refreshing twice as fast, real drops are counted in 60 fps units, so the counts understate them; compare the fps figure with the display's actual rate instead of trusting the drop count.
The JS thread is a single cashier and the UI thread is the shop floor. If the cashier is swamped, customers can still walk the aisles (scrolling) but nobody gets served (taps); if the floor is jammed, even an idle cashier cannot make the aisles move.
saying these in an interview costs you the question
- React Native has one thread, so one fps number covers everything
- If the UI fps is fine, the JS thread must be fine too
- Scroll jank with an idle JS thread must be a slow re-render
- On Android the overlay always shows a JS fps line
- A native-stack transition freezes whenever the JS thread is busy