EthicsComputerCourses
Join training +91 90532 10052

Performance Profiling

React Native · Lesson 27 of 30 Advanced 7 min read Updated

By the end of this lesson you will be able to measure whether a screen is really slow, tell whether the JavaScript side or the native side is the problem, find the component that renders too much, and apply the fixes that usually help.

The golden rule of performance work is: measure first, then fix. Guessing is like a mechanic replacing parts at random. A good mechanic listens to the engine first.

Step 1: measure in the right mode

Development mode adds many checks, warnings and debugging helpers. It can be several times slower than what users get. Before you decide something is slow, run it without development mode:

Terminal
# quick check: run the JavaScript bundle without dev mode and minified
npx expo start --no-dev --minify

# closest to what users get: a release build on a real Android phone
npx expo run:android --variant release

Also test on an ordinary phone, not only the latest flagship. Many of your users in India may have budget Android phones with less memory and slower processors. If the app feels smooth on a budget phone, it will fly on an expensive one.

Step 2: watch the two frame rates

Open the Dev Menu and turn on Perf Monitor. It shows two numbers that should stay close to the screen's refresh rate (often 60):

  • JS: frames per second on the JavaScript thread, where your React code runs.
  • UI: frames per second on the main (UI) thread, where native views are drawn.

Their pattern tells you where to look, like a doctor reading two different tests:

JS frame rateUI frame rateLikely cause
DropsFineToo much JavaScript work: big re-renders, heavy calculations, large JSON
FineDropsNative drawing work: huge images, many shadows or transparent layers, very deep view trees
Both dropBoth dropOften a long list with heavy rows, or animations not on the native side

The Perf Monitor is a guide only. For exact numbers, use the profilers below or Android Studio and Xcode.

Step 3: find extra renders with the React Profiler

Most JavaScript slowness in React apps comes from components rendering more often than needed, or doing heavy work each time they render. Here is that problem in miniature. It is plain React, so run it:

React JSXTry it yourself
import { useMemo, useState } from "react";
import { createRoot } from "react-dom/client";

let slowCalls = 0;
function slowGstReport(bills, who) {
  slowCalls++;
  console.log(`Slow GST report ran for ${who} (total calls: ${slowCalls})`);
  let busy = 0;
  for (let i = 0; i < 200000; i++) busy += i; // pretend this is heavy work
  return bills.reduce((sum, b) => sum + b * 0.18, 0);
}

const bills = [1200, 450, 3300];

function PlainReport() {
  const [dark, setDark] = useState(false);
  const gst = slowGstReport(bills, "PlainReport"); // runs on every render
  return (
    <div style={{ background: dark ? "#222" : "#fff", color: dark ? "#fff" : "#000", padding: 8 }}>
      <p>Without useMemo: GST ₹{gst.toFixed(2)}</p>
      <button onClick={() => setDark(!dark)}>Toggle plain theme</button>
    </div>
  );
}

function CachedReport() {
  const [dark, setDark] = useState(false);
  const gst = useMemo(() => slowGstReport(bills, "CachedReport"), []); // runs once
  return (
    <div style={{ background: dark ? "#222" : "#fff", color: dark ? "#fff" : "#000", padding: 8 }}>
      <p>With useMemo: GST ₹{gst.toFixed(2)}</p>
      <button onClick={() => setDark(!dark)}>Toggle cached theme</button>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<><PlainReport /><CachedReport /></>);
Output
Slow GST report ran for PlainReport (total calls: 1)
Slow GST report ran for CachedReport (total calls: 2)

Now click "Toggle plain theme" a few times: each click prints another line, because the heavy report runs on every render, even though only the colour changed. Click "Toggle cached theme" as often as you like: nothing new is printed, because useMemo reuses the first result.

In your app you find such components with the React Profiler in React Native DevTools (press j):

  1. Open the Profiler panel and click record.
  2. Do the slow action on the phone, such as typing in a search box.
  3. Stop recording and look at the flame chart. Wide or yellow bars are slow components; the list shows how many times each one rendered.
  4. In the Profiler settings, turn on "Record why each component rendered" to see which prop or state change caused each render.

Step 4: use the Performance panel and your own marks

From React Native 0.83, DevTools has a Performance panel. It records one timeline with your JavaScript, React's render work and network requests. You can add your own named marks with the standard performance API:

React JSX
export async function loadDashboard() {
  performance.mark("dashboard-start");

  const res = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/dashboard`);
  const data = await res.json();

  performance.mark("dashboard-end");
  performance.measure("Load dashboard", "dashboard-start", "dashboard-end");
  return data;
}

What you should see: in a Performance recording, a bar named "Load dashboard" appears in the timings track, showing exactly how long the request and parsing took, next to the renders that followed.

Fixes that usually work

Once you know the cause, these are the common fixes:

  • Lists: use FlatList or FlashList, stable keys, light rows and getItemLayout where heights are fixed (see SectionList & List Performance).
  • Re-renders: keep state close to where it is used, split big components, use selectors with Zustand, and let the React Compiler memoise.
  • Images: load images sized for the screen, not 4000-pixel originals, and use expo-image for caching.
  • Animations: use the native driver or Reanimated so they run on the UI thread.
  • Heavy work: do expensive calculations once (memoise), move them to the server, or mark updates as non-urgent with useTransition.
  • Logs: thousands of console.log calls slow a release build too. Remove them from hot paths.

Here is an example of a common fix: moving a big filter out of every render.

React JSX
import { useMemo, useState } from "react";
import { FlatList, Text, TextInput } from "react-native";

export function ProductSearch({ products }) {
  const [query, setQuery] = useState("");

  const results = useMemo(() => {
    const q = query.trim().toLowerCase();
    return q ? products.filter((p) => p.name.toLowerCase().includes(q)) : products;
  }, [products, query]);

  return (
    <>
      <TextInput value={query} onChangeText={setQuery} placeholder="Search 10,000 products" />
      <FlatList data={results} keyExtractor={(p) => p.id} renderItem={({ item }) => <Text>{item.name}</Text>} />
    </>
  );
}

Now the 10,000-item filter only runs when products or query change, not when anything else on the screen updates.

Common mistakes

  • Optimising before measuring. You may speed up code that was never the problem.
  • Measuring in development mode only. Always confirm in a release build.
  • Testing only on a fast phone. Keep a budget Android phone for testing.
  • Wrapping everything in useMemo and memo. It adds complexity; use the profiler and the React Compiler instead of guessing.
  • Huge images in small thumbnails. They waste memory and slow the UI thread.

Key points

  • Measure first, in a release or --no-dev build, on an ordinary phone.
  • The Perf Monitor shows JS and UI frame rates; which one drops tells you where to look.
  • The React Profiler shows which components render, how often and why.
  • The Performance panel (React Native 0.83 and newer) shows one timeline, including your performance.mark and measure entries.
  • Common fixes: better lists, fewer re-renders, smaller images, native-side animations and memoised heavy work.
  • The React Compiler, on by default in new Expo projects, removes much manual memoisation.

Exercise

A screen shows a list of 5,000 train stations with a search box, and typing feels slow on a budget phone. Write down, in order, the four steps you would take to find the cause (which tools, what you look for), and then write the code change you would make if the profiler shows that the filter runs on every keystroke and every row re-renders.

Show solution
React JSX
import { memo, useMemo, useState, useTransition } from "react";
import { FlatList, Text, TextInput } from "react-native";

const StationRow = memo(function StationRow({ name }) {
  return <Text style={{ height: 44, paddingHorizontal: 16 }}>{name}</Text>;
});

export function Stations({ stations }) {
  const [text, setText] = useState("");
  const [query, setQuery] = useState("");
  const [, startTransition] = useTransition();

  const results = useMemo(
    () => stations.filter((s) => s.name.toLowerCase().includes(query.toLowerCase())),
    [stations, query]
  );

  function onChange(value) {
    setText(value); // urgent: keep typing smooth
    startTransition(() => setQuery(value)); // the filter can wait
  }

  return (
    <>
      <TextInput value={text} onChangeText={onChange} placeholder="Search station" />
      <FlatList data={results} keyExtractor={(s) => s.code} renderItem={({ item }) => <StationRow name={item.name} />}
        getItemLayout={(_, i) => ({ length: 44, offset: 44 * i, index: i })} />
    </>
  );
}

Steps: run a release build on the budget phone; turn on Perf Monitor while typing to see that the JS frame rate drops; record with the React Profiler to see the filter and rows re-rendering; add performance marks if needed. The fix keeps typing urgent, defers the filter with a transition, memoises the filtered list and the rows, and gives the list fixed row heights.

Quick Quiz

1. Why should you measure performance in a release build?

2. The Perf Monitor shows the JS frame rate dropping while the UI frame rate stays high. What is the likely cause?

3. What does the React Profiler's flame chart help you find?

4. What does the React Compiler do in a new Expo project?

Learn React Native with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call