In React Native 0.87, how do performance.mark, performance.measure and PerformanceObserver let you time a screen in a release build?
answer
- Web Performance APIs, stable since 0.83
- mark instants, measure spans
- observer works in production builds
- entry types: mark, measure, event, longtask, resource
- longtask means over 50 ms
basics
~20 sReact Native implements the Web User Timing and Performance Timeline APIs, stable since 0.83: performance.mark records named instants, performance.measure records the span between them, and PerformanceObserver delivers those entries at runtime, including in production builds.
solid answer
~40 sSince 0.83 React Native ships a stable subset of the Web Performance APIs. I call `performance.mark('scores:fetch-start')` and `performance.mark('scores:fetch-end')` around the work, then `performance.measure('scores:fetch', 'scores:fetch-start', 'scores:fetch-end')`; a measure naming a mark that does not exist throws. A `PerformanceObserver` observing `mark` and `measure` receives the entries as they are recorded, and it also works in production builds, so the same code gives real-world numbers. React Native also reports `longtask` entries for JavaScript tasks over 50 ms and `event` entries for slow input events. In a development build the same marks and measures appear as User Timings in React Native DevTools' Performance panel. One caveat: `performance.now()` counts from system boot, not app start, so use differences, never absolute values.
code
tsx · 25 linesimport {useEffect, useState} from 'react';
import {FlatList, Text} from 'react-native';
type Match = {id: string; home: string; away: string; score: string};
export function ScoresScreen({loadMatches}: {loadMatches: () => Promise<Match[]>}) {
const [matches, setMatches] = useState<Match[]>([]);
useEffect(() => {
performance.mark('scores:fetch-start');
loadMatches().then(result => {
performance.mark('scores:fetch-end');
performance.measure('scores:fetch', 'scores:fetch-start', 'scores:fetch-end');
setMatches(result);
});
}, [loadMatches]);
return (
<FlatList
data={matches}
keyExtractor={m => m.id}
renderItem={({item}) => <Text>{`${item.home} ${item.score} ${item.away}`}</Text>}
/>
);
}go deeper
Recall that React Native has the Web performance.mark and performance.measure APIs and that PerformanceObserver receives the entries.
Explain mark versus measure, the observable entry types, the 50 ms long-task line, and why performance.now() values are only meaningful as differences.
Show how you instrument a critical screen in release builds, keep observer overhead and mark buffers under control, and use long tasks to find real-user stalls.
Discuss which user journeys deserve marks, how field measures feed a performance budget, and how you keep naming consistent across teams.
## What the APIs are React Native implements a subset of the **Web Performance APIs**, introduced in 0.82 and **stable since 0.83**: - **User Timing** — `performance.mark(name)` records a named instant; `performance.measure(name, startMark, endMark)` records the span between two marks, or between times given in an options object with `start`, `end`, `duration` and `detail`. - **Performance Timeline** — `PerformanceObserver`, plus `performance.getEntries()`, `getEntriesByType()` and `getEntriesByName()`. - **Event Timing** — `event` entries for slow user input events. - **Long Tasks** — `longtask` entries for long stretches of JavaScript work. `PerformanceObserver.supportedEntryTypes` returns `['mark', 'measure', 'event', 'longtask', 'resource']`. The 0.83 release post highlights the point that matters for profiling: **`PerformanceObserver` works in production builds**, so the numbers can come from the build users run rather than a dev-mode one. ## Timing the sports-scores screen 1. Put a `mark` where the work starts — the scores request is sent. 2. Put a `mark` where it ends — the response is parsed and handed to state. 3. Call `performance.measure` with the two mark names. If either mark is missing, `measure` throws a `SyntaxError` `DOMException` — guard it or keep both marks in one code path. 4. Register a `PerformanceObserver` once at app start for `measure` (and `longtask`), and send each entry's `name` and `duration` to your own telemetry. ## Reading an entry Every entry is a `PerformanceEntry` with four fields that matter: - **`name`** — the string you passed, such as `scores:fetch`; - **`entryType`** — `mark`, `measure`, `event`, `longtask` or `resource`; - **`startTime`** — when it began, on the `performance.now()` clock; - **`duration`** — zero for a mark, the span for a measure, the task or event length otherwise. A measure can also carry a `detail` value from its options object, which is cloned onto the entry — handy for tagging a measure with the number of rows rendered or the league being shown. ## Observer options worth knowing | Option or entry | Behaviour in React Native 0.87 | |---|---| | `observe({entryTypes: [...]})` | several types at once; cannot be combined with `type` or `durationThreshold` | | `observe({type, buffered, durationThreshold})` | one type; `buffered: true` also delivers entries recorded earlier | | `event` entries | since 0.86, `durationThreshold` defaults to 104 ms per the Event Timing spec | | `longtask` entries | JavaScript tasks longer than 50 ms | | callback's third argument | `{droppedEntriesCount}` — entries lost because a buffer was full | An observer that has used `entryTypes` cannot later call `observe` with `type`, and the reverse; React Native throws the same errors a browser does. The `event`, `longtask` and `resource` entries are kept in fixed-size circular buffers in 0.87 (150, 200 and 250 entries), so a slow observer can lose the oldest ones — which is exactly what `droppedEntriesCount` reports. ## Where the entries show up - **At runtime**, through `PerformanceObserver` — in debug and release builds. - **In React Native DevTools' Performance panel**, as custom User Timings on the same timeline as JavaScript execution and React tracks — but only in development builds, since DevTools is disabled in release. - **Not as sections in an Android Studio or Instruments trace** in a standard build. Native traces show threads; your marks live on the JavaScript side. ## Clocks and startup - **`performance.now()`** is partially supported: it counts milliseconds from system boot, not from app start, and `timeOrigin` is likewise boot-based. Durations and differences are fine; absolute values are not comparable across launches. - **`performance.rnStartupTiming`** is a non-standard React Native extension with `startTime`, `executeJavaScriptBundleEntryPointStart` and `endTime` for runtime initialisation — useful for capturing startup timing in release builds alongside your own marks. ## Hygiene - **Marks and measures accumulate** on the global timeline until you call `performance.clearMarks()` or `performance.clearMeasures()`; clear them after reporting on long-lived screens. - **Name marks by screen and phase** (`scores:fetch-start`) so entries are unambiguous in telemetry and in DevTools. - **Keep observer callbacks cheap** — they run on the JavaScript thread you are measuring. - **Compare like with like**: a measure from a release build on a low-end device is the number to budget against; a debug-build measure is inflated by dev mode.
- Why can't you pass durationThreshold when observing with entryTypes in React Native?React Native follows the Web spec: `durationThreshold` applies to a single-type observation such as `observe({type: 'event', durationThreshold: 50})`. Combining it with `entryTypes` throws a `TypeError`, as does mixing `type` and `entryTypes`. Use a separate observer per type when you need per-type options.
- What does a longtask entry tell you about a React Native screen?It records a stretch of JavaScript-thread work longer than 50 ms — several missed frames at 60 Hz. Collected in release through `PerformanceObserver`, long tasks show where real users hit JS-thread stalls; to find which function caused one, reproduce it and record a JavaScript profile.
saying these in an interview costs you the question
- performance.mark only works in debug builds with DevTools attached
- performance.now() counts from app start, so it works as a launch clock
- Marks automatically appear as sections in Android Studio system traces
- measure silently returns zero when a named mark is missing
- PerformanceObserver needs a third-party polyfill in React Native