Published September 5, 2026
6 min read
Intermediate
React Native & ExpoInteractive visualizer

React Native Performance Visualized: JS Work, Rendering & Dropped Frames

See how JavaScript, React rendering, native/UI work, lists, images, and state updates compete for a frame—and which optimizations actually help.

React Native Frame Timeline

Open in Engineering Lab
Loading visualizer...

A smooth mobile screen is a deadline problem. At 60 Hz the display presents a new frame about every 16.67 milliseconds. At 120 Hz the interval is about 8.33 milliseconds. Application work does not own that entire interval: the operating system, framework, layout, drawing, and other processes also need time.

The timeline above is an educational budget model, not a profiler. Its numbers are illustrative and its categories can overlap in real implementations. Use it to reason about where work originates, then use platform tooling to measure the app you actually ship.

What a dropped frame means

If the application cannot prepare the next visual state before a presentation deadline, the display may repeat an older frame. Repeated misses appear as stutter. One slow frame can be noticeable during a gesture; sustained overload makes scrolling or animation feel disconnected from touch.

“60 FPS” is therefore not a permanent property of an app. It is a stream of individual deadlines. An idle settings screen may be cheap while opening a list, decoding images, updating global state, or starting an animation creates a burst that misses several frames.

A modern React Native mental model

Avoid reducing current React Native to the historical slogan “JavaScript thread plus UI thread connected by a bridge.” Modern React Native and current Expo projects may use Hermes, Fabric, TurboModules, JSI-based integration, concurrent React capabilities, and library-specific worklets or native renderers. Exact scheduling and data movement depend on the versions and libraries in the app.

A durable model separates sources of cost:

  • JavaScript executes event handlers, application logic, parsing, and many library callbacks.
  • React decides which component output changes and prepares updates.
  • Native/platform systems perform layout, drawing, compositing, input, and module work.
  • GPU and image pipelines have their own constraints.

Improving one category does not erase the others. Moving an animation away from ordinary JS can protect it from a busy JS event handler, but a complex scene can still overload layout or rendering.

React renders are calculations

A state update schedules work. React calls affected components, reconciles their output, and commits necessary changes. A render does not necessarily mean every native view is recreated, but calling many expensive component functions still consumes time.

TSX
const Row = memo(function Row({ item, onPress }: RowProps) {
  return <Pressable onPress={() => onPress(item.id)}>{/* ... */}</Pressable>;
});

memo can skip a render when props compare equal. It cannot help when every render supplies a new object or function, when the component reads frequently changing context, or when comparison costs more than rendering. Stabilize data flow for a reason; do not wrap every value in useMemo and useCallback by reflex.

Unnecessary re-renders begin with state boundaries

If a high-frequency value lives at the root of a large screen, every update may invalidate a broad subtree. Better options include keeping transient state near its owner, splitting context by update frequency, selecting only the store slice a component needs, and separating frequently changing presentation from expensive static content.

State architecture is a performance tool because it decides who hears about a change. It is also a correctness and maintainability tool, so judge the full tradeoff. A complicated external store is not justified by one cheap extra render.

Large lists need bounded work

Mapping hundreds of records into a ScrollView asks React Native to create work for all of them, including items far outside the viewport. A virtualized list keeps a moving window of visible rows plus a buffer.

TSX
<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  renderItem={renderItem}
  getItemLayout={fixedRowLayout}
/>

The defaults are not universally optimal. Window size, batch size, row complexity, fixed layout information, and clipping trade memory, blank areas, responsiveness, and mount bursts. Use stable keys; paginate or incrementally load data; avoid nesting virtualized lists with incompatible scrolling; test on lower-end physical devices.

Images consume more than network time

An image request may be cached and still cost memory, decoding, resizing, upload, and compositing work. A compressed 200 KB file can decode into several megabytes of pixels. Request appropriately sized assets, reserve layout dimensions, cache intentionally, avoid mounting many full-resolution images at once, and verify whether format and library behavior match each platform.

Network work is mostly waiting, but response parsing and the state update it triggers use CPU. Returning 5,000 records creates a network payload, JSON parsing work, memory pressure, reconciliation work, and potentially thousands of rows. Backend pagination can be a UI performance optimization.

Long-running JavaScript

As JavaScript Event Loop Visualized explains, async makes waiting composable; it does not move CPU-heavy work off JavaScript. Large synchronous transforms, JSON parsing, or loops can delay JS-driven input and updates.

Prefer doing less work, doing it incrementally, precomputing at a better layer, or moving a genuinely heavy operation to an appropriate native/worker facility. Each boundary adds serialization, cancellation, lifecycle, and error-handling costs, so “move it off thread” is an architecture decision rather than a free switch.

An Expo-oriented workflow

The portfolio's current Expo projects include modern SDK-era applications, but this tutorial avoids promising one internal threading diagram across every Expo SDK and library. Profile the release-like build: development instrumentation and debugging can change timings substantially.

A practical loop is:

  1. Reproduce one visible symptom on representative hardware.
  2. Record a trace or profiler session around that interaction.
  3. Identify whether the bottleneck is JS, React renders, native/UI work, images, GPU work, or allocation pressure.
  4. Make one focused change and measure the same interaction again.
  5. Keep the optimization only if it improves the target without unacceptable complexity or behavior changes.

Useful interventions include narrowing state subscriptions, virtualizing lists, reducing per-row work, resizing images, moving static calculations out of render, deferring non-urgent work, and choosing animation/rendering libraries whose execution model fits the interaction.

Common misconceptions

  • Every re-render is bad. Cheap renders are normal; expensive or needlessly broad ones are candidates for work.
  • Memoization always improves performance. It adds comparison, memory, dependency, and maintenance cost.
  • Network calls block frames. Waiting usually does not, but parsing and applying large results can.
  • Native is automatically fast. Native code, layout, drawing, and GPU work can miss deadlines too.
  • One simulator profile proves performance. Device class, thermal state, release mode, refresh rate, and real data matter.

Key takeaways

  • A frame rate is a sequence of deadlines, not a single average number.
  • Locate the source of cost before selecting an optimization.
  • State boundaries determine the blast radius of updates.
  • Virtualization bounds list work; it does not make complex rows free.
  • Images have decode and memory costs beyond download size.
  • Measure release-like builds on physical devices and preserve the simplest design that meets the budget.