skip to content

A React Native live sports-scores screen stutters on Android as scores stream in; how do you use an Android Studio system trace to find which thread is at fault?

level: seniorimportance: should knowfreq 38%

answer

  1. profileable build, real device
  2. Capture System Activities task
  3. frame boundaries in the trace
  4. mqt_v_js vs UI thread vs RenderThread
  5. long DrawFrame means GPU work

basics

~20 s

Capture a system trace from a profileable build on a real device, then compare threads against frame boundaries: the JavaScript thread (mqt_v_js) running across frames means a JavaScript problem; the UI thread or RenderThread overrunning means native view or GPU work.

solid answer

~50 s

I run the release variant as profileable, put the device on the scores screen, start the Android Studio Profiler's **Capture System Activities** task, let a few score updates arrive, and stop. In the trace, or in Perfetto after exporting it, I find my process and three threads: the UI thread named after the package, the JavaScript thread (`mqt_v_js` in the New Architecture runtime) and `RenderThread`. Then I read them against the frame boundaries. If `mqt_v_js` is busy across boundaries while the others idle, the problem is JavaScript and I move to a JavaScript profile to find the function or component. If the UI thread or `RenderThread` overruns — long `DrawFrame` slices — it is native: too much GPU work per frame, or views being created mid-interaction. The trace tells me where to look next; it doesn't name the component.

go deeper

for a junior

Know that Android traces come from the Android Studio Profiler and that they show the JavaScript thread, the UI thread and RenderThread side by side.

for a middle

Explain how to capture from a profileable build and how to read slices against frame boundaries to tell JavaScript overruns from native ones.

for a senior

Diagnose from the trace: separate JavaScript, GPU and view-creation patterns, pick the next tool for each, and confirm the fix with a second release-like trace.

for a principal

Decide when native traces become part of performance reviews, which low-end reference devices the team captures on, and how findings feed frame budgets.

## What a system trace is for A **system trace** records what every thread in the process did, slice by slice, on one shared clock. For a React Native screen that stutters, it answers the first question of any jank investigation: **is the time going into JavaScript or into native rendering?** Every later step depends on that answer, and no JavaScript-only tool can give it. At 60 Hz each frame has about **16.7 ms**. A frame is dropped when the work it needs has not finished by the next frame boundary. ## Capturing the trace The React Native profiling guide's procedure, adapted to the scores screen: 1. **Use a physical device** that shows the stutter, connected over USB. 2. **Run the app as profileable** from Android Studio (open the project's `android` folder), with the **release** variant selected. Profileable lets the profiler attach with little overhead and without a debuggable build's costs; the release variant keeps JavaScript out of dev mode. 3. **Navigate to the scores screen**, then start the **Capture System Activities** task in the Profiler pane. 4. **Let several score updates arrive** while you scroll — reproduce the exact stutter — then press *Stop recording*. 5. **Inspect** in Android Studio, or select the recording under *Past Recordings*, choose *Export recording*, and open it in **Perfetto**. The standalone `systrace` tool is gone from Android platform-tools; this is its replacement. ## Finding the right threads | Thread | How it is named | What runs there | |---|---|---| | UI (main) thread | your package name, or UI thread | Android measure, layout and draw, `Choreographer` frame callbacks, mounting native views | | JavaScript thread | `mqt_v_js` in the New Architecture runtime | Hermes executing your React code | | Native modules thread | `mqt_v_native` | native module work queued off the JavaScript thread | | RenderThread | `RenderThread` | the GPU commands for each frame (`DrawFrame`, `queueBuffer`) | Older guides and screenshots show `mqt_js` and `mqt_native_modules`; those are the legacy architecture's names. React Native 0.87's runtime names its queues `mqt_v_js` and `mqt_v_native`. Kernel name limits can truncate the package name, and a thread may show as `<...>` — identify it by the slices it runs. ## Reading it against frame boundaries Turn on frame or VSync highlighting so each frame period is visible, then look at the frames where the stutter happened: - **JavaScript is the bottleneck** when `mqt_v_js` is busy almost continuously and its slices cross frame boundaries while the UI thread waits. On a scores screen this is typical when every update re-renders the whole list or reformats every row. - **Native rendering is the bottleneck** when the UI thread or `RenderThread` has slices crossing boundaries. Long `DrawFrame` time means the GPU is still draining the previous frame's work. - **New views mid-interaction** look like a burst of JavaScript work followed by an expensive traversal on the UI thread — new rows or panels being created and mounted while the user scrolls. ## A worked reading of the scores screen Suppose each score push arrives about once a second and the stutter lasts a few frames each time. In the trace: 1. **Zoom to one push.** Find the first frame that runs late after the update arrives. 2. **Check `mqt_v_js` first.** If a single long slice starts with the update and runs through three or four frame periods, JavaScript owns those dropped frames. 3. **Then check the UI thread and `RenderThread`.** If they finish their own work early in each frame and then wait, they are victims, not causes. 4. **Repeat on a second push.** One slow frame can be noise; the same pattern on every push is the bug. ## What to do with each verdict - **JavaScript:** the trace shows engine frames, not your functions. Record a Hermes sampling profile of the same interaction to find the hot function, and a React commit profile to find the component that renders too much; then re-measure in release. - **GPU work:** the guide suggests `renderToHardwareTextureAndroid` for complex, static content that is being animated, and making sure `needsOffscreenAlphaCompositing` is not enabled — it is off by default and greatly increases per-frame GPU load. - **View creation:** postpone creating new UI until after the interaction, or simplify what is created. - **Unexplained native CPU:** *Find CPU Hotspots (Java/Kotlin Method Recording)* shows which native methods run, but it is slow, so read proportions, not durations. ## Common mistakes - Capturing from a debug build, where dev-mode JavaScript makes `mqt_v_js` look guilty of everything. - Looking for your component names in the trace; they are not there. - Blaming the UI thread because it is busy, without checking whether its work actually crosses a frame boundary. - Recording minutes of activity instead of the few seconds around the stutter, which buries the frames that matter.

  • Your trace shows long DrawFrame slices on RenderThread while scores animate in React Native. What do you change first?
    Reduce per-frame GPU work. Check that no animated view has `needsOffscreenAlphaCompositing` enabled — it is off by default and costly. For complex content that moves but does not change, try `renderToHardwareTextureAndroid` while it animates and turn it off afterwards, since it costs memory. Then capture another trace to confirm frames fit.
  • The trace shows mqt_v_js saturated during each score update. Why isn't the trace enough to fix it?
    A system trace sees Hermes engine frames, not your JavaScript functions or React components. It proves the JavaScript thread is the bottleneck; a Hermes sampling profile shows which function is hot, and a React commit profile shows which component renders too much. Fix, then re-measure in a release-like build.
  • Why do some React Native guides show a JavaScript thread called mqt_js while your trace shows mqt_v_js?
    The names come from React Native's message-queue threads. The legacy bridge set up `mqt_js` and `mqt_native_modules`; the New Architecture runtime, the only one since 0.82, creates `mqt_v_js` and `mqt_v_native`. Older docs and screenshots predate that.

saying these in an interview costs you the question

  • Capture the system trace from a debug build for convenience
  • The trace shows which React component rendered slowly
  • A busy UI thread always means the native side is at fault
  • needsOffscreenAlphaCompositing is on by default and must be disabled
  • Look for the JavaScript thread under the name mqt_js in 0.87