react-native-best-practices

star 0

React Native performance optimization guide covering JavaScript/React patterns, native module usage, bundle optimization, and profiling. Use when building or optimizing React Native applications. Adapted from Callstack Agent Skills.

Sharjeelbaig By Sharjeelbaig schedule Updated 2/16/2026

name: react-native-best-practices description: React Native performance optimization guide covering JavaScript/React patterns, native module usage, bundle optimization, and profiling. Use when building or optimizing React Native applications. Adapted from Callstack Agent Skills.

React Native Best Practices Skill

Adapted from Callstack React Native Agent Skills.

Priority-ordered guidelines for building fast, efficient React Native applications.


Priority 1: Rendering & Re-Render Optimization

Minimize Re-Renders

The biggest React Native performance issue. Every re-render triggers layout, diffing, and potentially native view updates.

// ❌ Inline objects cause re-renders on every parent render
function Parent() {
  return <Child style={{ flex: 1, padding: 16 }} />;
}

// ✅ Stable reference with StyleSheet
const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
});

function Parent() {
  return <Child style={styles.container} />;
}

Memoize Expensive Components

// ✅ Only re-renders when item or onPress change
const ListItem = React.memo(function ListItem({ item, onPress }) {
  return (
    <Pressable onPress={() => onPress(item.id)}>
      <Text>{item.title}</Text>
    </Pressable>
  );
});

Stable Callbacks with useCallback

// ❌ New function reference every render → ListItem re-renders
function List({ items }) {
  const handlePress = (id) => selectItem(id);
  return items.map(item => <ListItem key={item.id} onPress={handlePress} item={item} />);
}

// ✅ Stable reference
function List({ items }) {
  const handlePress = useCallback((id) => selectItem(id), []);
  return items.map(item => <ListItem key={item.id} onPress={handlePress} item={item} />);
}

React Compiler (Automatic Memoization)

If using React 19+, enable the React Compiler for automatic memoization. It eliminates the need for manual React.memo, useMemo, and useCallback in most cases.

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {}],
  ],
};

Incremental adoption: start with specific files or directories rather than the entire codebase.

// babel.config.js — opt-in per file
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      sources: (filename) => {
        return filename.includes('src/components/');
      },
    }],
  ],
};

Priority 2: Animations on the UI Thread

Use Reanimated for All Animations

Never use the built-in Animated API for performance-critical animations. Reanimated runs animations on the UI thread, avoiding JS thread bottlenecks.

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';

function AnimatedCard() {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  const onPressIn = () => {
    scale.value = withSpring(0.95);
  };
  const onPressOut = () => {
    scale.value = withSpring(1);
  };

  return (
    <Pressable onPressIn={onPressIn} onPressOut={onPressOut}>
      <Animated.View style={[styles.card, animatedStyle]}>
        <Text>Press me</Text>
      </Animated.View>
    </Pressable>
  );
}

Worklets for UI Thread Logic

import { runOnUI } from 'react-native-reanimated';

// This function runs on the UI thread
function updatePositionWorklet(x, y) {
  'worklet';
  position.value = { x, y };
}

// Call from JS thread
runOnUI(updatePositionWorklet)(100, 200);

When to Use What

Animation Type Use
Simple opacity/transform Reanimated withTiming/withSpring
Gesture-driven Reanimated + Gesture Handler
Scroll-driven Reanimated useAnimatedScrollHandler
Layout animations Reanimated Layout Transitions
Lottie animations lottie-react-native
Complex sequences Reanimated + withSequence/withDelay

Priority 3: List Performance

Use FlashList Over FlatList

import { FlashList } from '@shopify/flash-list';

function UserList({ users }) {
  return (
    <FlashList
      data={users}
      renderItem={({ item }) => <UserCard user={item} />}
      estimatedItemSize={80}
      keyExtractor={(item) => item.id}
    />
  );
}

List Optimization Checklist

  • Always provide keyExtractor
  • Use estimatedItemSize (FlashList) or getItemLayout (FlatList)
  • Avoid inline functions in renderItem
  • Memoize renderItem components with React.memo
  • Set removeClippedSubviews={true} for long lists
  • Use windowSize prop to control render-ahead distance
  • Never nest scrollable lists — use section headers instead

Priority 4: Native Modules Over Polyfills

Replace JS Polyfills with Native SDKs

JS polyfills are significantly slower than native implementations. Always prefer native modules.

Task ❌ JS Polyfill ✅ Native Module
Internationalization intl polyfill expo-localization + react-native-intl
Crypto crypto-browserify react-native-quick-crypto
Date formatting moment.js (500KB) Intl.DateTimeFormat (native)
Navigation JS-based routing react-native-screens + native navigation
Image processing JS canvas operations react-native-fast-image
Storage AsyncStorage for large data react-native-mmkv
SQLite JS-based SQL expo-sqlite or react-native-quick-sqlite

Decision Matrix

Use a native module when:

  1. The operation is CPU-intensive (crypto, image processing)
  2. There's a well-maintained native library
  3. The polyfill adds >50KB to bundle
  4. The feature requires frequent calls (storage, formatting)

Keep JS when:

  1. The logic is simple and rarely called
  2. No native alternative exists
  3. Cross-platform consistency is more important than speed

Priority 5: Bundle Size Optimization

Analyze Bundle Contents

# Generate bundle stats
npx react-native-bundle-visualizer

# Or with Expo
npx expo export --dump-sourcemap

Tree-Shaking Best Practices

// ❌ Imports entire library
import _ from 'lodash';
const result = _.map(data, fn);

// ✅ Import only what you need
import map from 'lodash/map';
const result = map(data, fn);

Lazy Loading Screens

import { lazy, Suspense } from 'react';

const SettingsScreen = lazy(() => import('./screens/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}>
      <Navigator>
        <Screen name="Settings" component={SettingsScreen} />
      </Navigator>
    </Suspense>
  );
}

Hermes Engine Best Practices

Hermes is the default JS engine for React Native. Optimize for it:

  • Prefer for loops over Array.map() for hot paths
  • Use Intl APIs (supported natively in Hermes)
  • Avoid eval() and new Function() — not supported
  • Use bytecode precompilation for faster startup

Priority 6: Startup Performance (TTI)

Reduce Time-to-Interactive

  1. Minimize root component complexity — render a skeleton first, load data after
  2. Defer non-critical initialization — analytics, deep linking, crash reporting can initialize after first render
  3. Use InteractionManager.runAfterInteractions() for deferred work
useEffect(() => {
  InteractionManager.runAfterInteractions(() => {
    // Heavy work after animations complete
    prefetchData();
    initializeAnalytics();
  });
}, []);
  1. Preload critical assets — fonts, images used on first screen
  2. Minimize bridge calls at startup (old architecture) — batch native calls

Splash Screen Strategy

import * as SplashScreen from 'expo-splash-screen';

SplashScreen.preventAutoHideAsync();

function App() {
  const [ready, setReady] = useState(false);

  useEffect(() => {
    async function prepare() {
      await loadFonts();
      await prefetchCriticalData();
      setReady(true);
      SplashScreen.hideAsync();
    }
    prepare();
  }, []);

  if (!ready) return null;
  return <MainNavigator />;
}

Profiling & Debugging

React DevTools Profiler

  1. Open React DevTools → Profiler tab
  2. Record a user interaction
  3. Look for:
    • Components rendering more than once per interaction
    • Components taking >16ms to render
    • Unnecessary renders (no visual change)

Flame Graph Interpretation

  • Wide bars: Expensive renders — optimize or memoize
  • Repeated bars: Same component re-rendering — check props stability
  • Deep trees: Too many nested components — flatten hierarchy

Native Profiling

  • iOS: Xcode Instruments → Time Profiler, Allocations
  • Android: Android Studio Profiler → CPU, Memory

Common Pitfalls

Symptom Cause Fix
Janky scrolling Heavy renderItem Memoize, simplify component
Slow screen transitions Large component trees Lazy load, defer rendering
High memory usage Image caching Use FastImage with cache limits
JS thread blocked Expensive computation Move to worklet or native module
Slow startup Too much initialization Defer non-critical work

State Management

Atomic State (Jotai/Zustand)

Prefer atomic state libraries over Redux for React Native:

// Jotai — atomic state
import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return <Button onPress={() => setCount(c => c + 1)} title={`${count}`} />;
}
// Zustand — simple global state
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

function Counter() {
  const count = useStore((s) => s.count);
  const increment = useStore((s) => s.increment);
  return <Button onPress={increment} title={`${count}`} />;
}

Zustand Selector Pattern

Always use selectors to prevent unnecessary re-renders:

// ❌ Re-renders on ANY store change
const { count, user, theme } = useStore();

// ✅ Re-renders only when count changes
const count = useStore((s) => s.count);

Concurrent React Patterns

useDeferredValue for Expensive Renders

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const results = useMemo(() => filterItems(deferredQuery), [deferredQuery]);

  return (
    <View style={{ opacity: query !== deferredQuery ? 0.8 : 1 }}>
      <FlashList data={results} renderItem={renderItem} estimatedItemSize={60} />
    </View>
  );
}

useTransition for Navigation

function TabBar() {
  const [isPending, startTransition] = useTransition();
  const [activeTab, setActiveTab] = useState('home');

  function switchTab(tab) {
    startTransition(() => {
      setActiveTab(tab); // non-urgent, can be interrupted
    });
  }

  return (
    <View style={{ opacity: isPending ? 0.7 : 1 }}>
      <TabContent tab={activeTab} />
    </View>
  );
}

Quick Reference

Metric Target Tool
FPS 60 (≤16ms/frame) React DevTools Profiler
Re-renders 1 per interaction React.memo, useCallback
Bundle Size <5MB JS bundle Bundle Visualizer
TTI <2s cold start Splash screen + deferred init
List scroll No jank FlashList + estimatedItemSize
Problem Skill to Apply
Janky animations Reanimated (UI thread)
Slow lists FlashList + memoization
Large bundle Tree-shaking + lazy loading
Slow JS operations Native modules
Re-render cascades Atomic state + selectors
Slow startup InteractionManager + deferred init

Attribution

Based on Callstack React Native Agent Skills — React Native Performance Optimization.

Install via CLI
npx skills add https://github.com/Sharjeelbaig/developer-ai --skill react-native-best-practices
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
Sharjeelbaig
Sharjeelbaig Explore all skills →