← All shorts
42React Native 0.73+2 min read

Profiling Render Performance and Dropped Frames

Finding a jank source means correlating a dropped-frame timeline with which JS render or native operation was running then.

A render count in the console tells you one thing: something re-rendered.

It won't tell you whether that render dropped a frame, or what else was fighting for the JS thread at that instant.

#The old way

import { useEffect, useRef } from 'react';
import { View, Text } from 'react-native';

function ProductCard({ title, price }: { title: string; price: number }) {
  const renderCount = useRef(0);
  renderCount.current += 1;

  useEffect(() => {
    console.log(`ProductCard rendered ${renderCount.current} times`);
  });

  return (
    <View>
      <Text>{title}</Text>
      <Text>${price.toFixed(2)}</Text>
    </View>
  );
}

#The new way

import { useEffect } from 'react';
import { View, Text } from 'react-native';
import { performance } from 'react-native-performance';

function ProductCard({ title, price }: { title: string; price: number }) {
  useEffect(() => {
    performance.mark('product-card-start');
    return () => {
      performance.mark('product-card-end');
      performance.measure('product-card-mount', 'product-card-start', 'product-card-end');
    };
  }, []);

  return (
    <View>
      <Text>{title}</Text>
      <Text>${price.toFixed(2)}</Text>
    </View>
  );
}
import { PerformanceObserver } from 'react-native-performance';

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 16) {
      console.warn(`${entry.name} took ${entry.duration.toFixed(1)}ms`);
    }
  }
});

observer.observe({ entryTypes: ['measure'] });

The marks share a clock with native frame traces. So you can line up product-card-mount against Instruments' Time Profiler on iOS, or Perfetto on Android, and read off exactly what native work was running during a dropped frame.

#Why it matters

  • A timestamped mark points at one specific dropped frame. A render count points at nothing in particular.
  • Anything over roughly 16ms is one frame at 60fps, so that threshold surfaces real jank candidates and skips the noisy re-renders.
  • Correlating a JS mark with a native trace tells you where the cost actually sits: your component, or a native commit underneath it.

#Gotcha

performance.now() and your marks run on the JS thread's clock. A dropped frame is a native/UI-thread event.

That gap bites. A render can measure fast in JS and still drop a frame, because it triggered an expensive synchronous native layout pass. Fabric committing a large tree does exactly that. Cross-check JS-side measures against a native trace rather than trusting JS timing alone.

react-nativeperformancedebugging

Related shorts