skip to content

In React Native, a Hermes sampling profile recorded in a debug build blames a score-formatting helper for scores-screen jank; how do you read it and confirm the finding?

level: seniorimportance: should knowfreq 30%

answer

  1. periodic snapshots of the JS stack
  2. proportions, not exact durations
  3. self time versus total time
  4. JavaScript thread only
  5. confirm with measure in release

basics

~20 s

A Hermes sampling profile periodically records the JavaScript call stack, so it shows each function's share of JS-thread time, not exact durations or native work. Check the helper's self time, discount dev-mode frames, then confirm with performance.measure in a release build.

solid answer

~50 s

A Hermes sampling profile — in 0.87 recorded through React Native DevTools' Performance panel — interrupts the JavaScript thread at a high rate and records the call stack each time. A function's weight is how many samples it appears in, so I read **self time** (samples where it is on top) to find the real hot spot and **total time** to find the caller that triggers it. If the formatter has high self time and is called once per row per update, that is my candidate. But the profile came from a debug build, so React's dev-only checks and on-device compilation inflate everything, and it sees only the JavaScript thread. So I wrap the formatting pass in `performance.mark`/`performance.measure`, collect it with `PerformanceObserver` in a release build, and compare before and after the fix — ideally with a native trace too, to see the frames recover.

go deeper

for a junior

Know that a sampling profile shows which JavaScript functions take the most time, and that React Native records it through React Native DevTools.

for a middle

Explain sampling versus exact timing, self time versus total time, and why the profile covers only the JavaScript thread.

for a senior

Show the full loop: find the candidate in a debug profile, discount dev-mode noise, confirm and size it with measures in a release build, then verify the frames recover.

for a principal

Discuss how teams avoid optimising dev-mode artefacts: which findings need release confirmation, and how profiling results turn into regression checks.

## What a sampling profile is A **sampling profiler** does not time every function call. It interrupts the program at a fixed rate and records the **call stack** at that instant. Functions that run for a long time appear in many samples; functions that run briefly appear in few or none. Adding the samples up gives each function's **share** of the recorded time. **Hermes**, React Native's default engine (Hermes V1 since 0.84), has a built-in sampling profiler. In React Native 0.87 you usually reach it through **React Native DevTools' Performance panel** (since 0.83): recording a trace turns Hermes sampling on — the 0.87 source requests 10,000 samples per second — and the result appears as the *JavaScript execution* track next to React tracks and your User Timings. The old `react-native profile-hermes` command and its `.cpuprofile` workflow were removed from the Community CLI, as announced with 0.75. ## Reading it: self time, total time, call sites | Measure | Meaning | Use it to | |---|---|---| | **Self time** | samples where the function itself is on top of the stack | find where the CPU actually burns | | **Total time** | samples where the function is anywhere on the stack | find which caller triggers the expensive work | | **Call pattern** | many short calls versus one long call | tell per-row work from a single heavy pass | For the scores screen: 1. **Select the stutter**, not the whole recording — the window where updates arrived. 2. **Sort bottom-up by self time.** A formatting helper near the top with high self time is the burner. 3. **Walk up to its callers.** If it is called from each row's render for every row on every update, the cost multiplies with list length and update rate. 4. **Cross-check React tracks** on the same timeline: does the heavy stretch sit inside a commit of the scores list? ## What it cannot tell you - **Exact durations of short calls.** It is statistical; a function that finishes between samples can be missed, and small shares are noisy. Treat it as proportions. - **Native work.** It samples only the JavaScript thread. Layout, drawing and GPU time on the UI thread and RenderThread are invisible; that is what Instruments or an Android system trace is for. - **Release-build magnitudes.** DevTools is disabled in release builds, so the recording comes from a development build: `__DEV__` checks, warnings and on-device compilation of Metro-served source inflate the numbers, and React's own frames look heavier than they will in production. - **Which component re-rendered and why.** That is the React DevTools Profiler's question. ## Confirming before and after the fix 1. **Instrument the suspect.** Put `performance.mark` before and after the formatting pass and a `performance.measure` over it. 2. **Collect in a release build** with a `PerformanceObserver`, which works in production builds since 0.83, on a real low-end device. 3. **Fix the pattern the profile revealed** — format once when data arrives instead of on every render, or format only the rows that changed — and measure again. 4. **Check the frames.** A native trace should now show the JavaScript thread finishing inside the frame boundaries during updates. ## Fix patterns the profile typically points to The profile tells you *what* is expensive; the shape of the call pattern suggests the fix: - **Per-row, per-update formatting** — format once when the data arrives and store the display string, so rendering a row does no formatting at all. - **Whole-list recomputation for one changed score** — derive new display data only for the matches that changed. - **One long parse of a large payload** — shrink the payload or split the work so it does not land in a single long task. Whichever you choose, the profile that found the problem is not the proof that you fixed it; the release-build measure is. ## Traps - **Chasing dev-only frames.** Validation and warning code shows up in debug profiles; it is gone in release. - **Reading total time as cost.** A root component's total time is high by definition; self time points at the work. - **Profiling too long.** A minute of idle samples dilutes the half-second that matters. - **Assuming one run is enough.** Record the interaction a few times; sampling noise can reorder small entries.

  • Why can a function that is costly per call still barely appear in a Hermes sampling profile?
    Sampling counts how often a function is on the stack at sample instants, so its weight reflects its total time across the recording. A function called only once or twice contributes few samples even if each call is slow, and very short calls can fall between samples. To time one specific call precisely, wrap it in `performance.measure`.
  • The profile shows high total time in the scores list component but low self time. What does that mean?
    The list component itself does little work; the time is in functions it calls — row renders, formatters, child components. Total time says the expensive work happens beneath it; self time on its descendants says where. Walk down the tree, or sort bottom-up by self time, to find the function actually burning CPU.

A sampling profile is like a photographer snapping the kitchen at fixed intervals: dishes that take a long time appear in many photos, while a garnish added between two snaps never shows up. You learn which dishes dominate the evening, not the exact seconds each one took.

saying these in an interview costs you the question

  • A sampling profile gives the exact duration of every function call
  • The Hermes profile also shows UI-thread layout and GPU time
  • The function with the highest total time is the one to optimise
  • Debug-build profile numbers match what release users experience
  • Use react-native profile-hermes to capture the profile in 0.87