Claude Skill

mobile-performance-react-native

React Native performance profiling, optimization, and monitoring - JS/UI thread analysis, re-render prevention, list optimization, image performance, bundle size, startup time, memory leaks, React Compiler, New Architecture benefits

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-performance-react-native_skills_mobile-performance-react-native-3a51ef5.zip · 21 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-performance-react-native/skills/mobile-performance-react-native
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

React Native Performance Patterns

Quick Guide: Profile before optimizing -- use React Native DevTools Profiler (replaces Flipper since 0.76) and platform profilers to find actual bottlenecks. Target 60 FPS (16.67ms per frame). Understand JS thread vs UI thread: animations on the UI thread, business logic on JS. Use React.memo + useCallback for list items, FlashList for large lists, InteractionManager to defer heavy work during transitions. React Compiler (v1.0+) auto-memoizes most components -- verify before adding manual memoization. Always test in release builds; dev mode adds significant overhead.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST profile BEFORE optimizing -- use React Native DevTools Profiler or platform tools to identify actual bottlenecks, never optimize blindly)

(You MUST test performance in RELEASE builds -- dev mode adds significant overhead that masks real performance characteristics)

(You MUST understand the JS thread vs UI thread distinction -- animations belong on the UI thread, heavy computation must be deferred with InteractionManager)

(You MUST memoize renderItem callbacks and item components for FlashList/FlatList -- inline functions break virtualization performance)

(You MUST check if React Compiler is enabled before adding manual useMemo/useCallback/React.memo -- the compiler auto-memoizes and manual hints become redundant)

</critical_requirements>


Auto-detection: React Native performance, FPS, frame rate, JS thread, UI thread, re-render, React.memo, useMemo, useCallback, FlashList optimization, FlatList optimization, InteractionManager, requestAnimationFrame, Hermes, bytecode, bundle size, Metro, tree shaking, startup time, memory leak, heap snapshot, React Compiler, auto-memoization, react-native-performance, Flipper profiler, React Native DevTools, Perf Monitor, useNativeDriver, LayoutAnimation, Reanimated worklet

When to use:

  • Diagnosing dropped frames, jank, or slow transitions
  • Optimizing list scrolling performance (FlashList/FlatList)
  • Reducing re-renders in component trees
  • Profiling JS thread vs UI thread bottlenecks
  • Reducing app startup time or bundle size
  • Detecting and fixing memory leaks
  • Deciding whether to add manual memoization vs relying on React Compiler
  • Monitoring performance in production

When NOT to use:

  • General React Native component architecture (use the framework skill)
  • Navigation setup and patterns (use the framework skill)
  • Styling and theming patterns (use the framework skill)
  • Animation API patterns (use the animation skill)

Key patterns covered:

  • JS thread vs UI thread mental model and frame budget
  • Profiling with React Native DevTools, platform profilers, and Hermes profiles
  • Re-render optimization (React.memo, useCallback, useMemo, React Compiler)
  • List optimization (FlashList cell recycling, FlatList tuning, memoized items)
  • Image optimization (sizing, caching, preloading, placeholder strategies)
  • Bundle size reduction (tree shaking, named imports, code splitting)
  • Startup time optimization (Hermes bytecode, lazy loading, deferred work)
  • Memory leak detection and prevention
  • Production performance monitoring

Detailed Resources:




<decision_framework>

Decision Framework

Should I Optimize This?

Is there a measurable performance problem?
|-- NO -> Don't optimize. Premature optimization wastes time.
+-- YES -> Have you profiled to identify the root cause?
    |-- NO -> Profile first (React Native DevTools Profiler, Perf Monitor)
    +-- YES -> What is the bottleneck?
        |-- JS thread congested -> Defer work (InteractionManager), reduce re-renders
        |-- UI thread dropping frames -> Offload to native (useNativeDriver, worklets)
        |-- List scrolling jank -> FlashList, memoize renderItem, getItemType
        |-- Slow startup -> Lazy load screens, defer initialization
        |-- High memory usage -> Check for leaks (heap snapshots)
        +-- Large bundle -> Named imports, tree shaking, bundle visualization

Manual Memoization Decision

Is React Compiler enabled in your project?
|-- YES -> Does the component show "Memo" badge in DevTools?
|   |-- YES -> Manual memoization is redundant. Don't add it.
|   +-- NO -> Is this a third-party component or complex dynamic pattern?
|       |-- YES -> Manual memo/useCallback may be needed. Profile first.
|       +-- NO -> The compiler should handle it. File a bug if it doesn't.
+-- NO -> Is this component in a list (renderItem)?
    |-- YES -> Always React.memo + useCallback
    +-- NO -> Does profiling show unnecessary re-renders?
        |-- YES -> Add React.memo, useCallback for callback props
        +-- NO -> Don't memoize. It adds complexity without benefit.

Animation Performance Decision

What type of animation?
|-- Layout change (appear/disappear) -> LayoutAnimation (Core Animation, bypasses JS)
|-- Simple transform/opacity -> Animated API with useNativeDriver: true
|-- Gesture-driven -> Use your animation library's worklet-based API (runs on UI thread)
+-- Complex multi-step -> Use your animation library for UI thread execution

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Optimizing without profiling first -- you're guessing, not solving. Profile to identify the actual bottleneck.
  • Testing performance in dev mode -- dev mode adds significant overhead (console logging, error checking, hot reload). Always benchmark in release builds.
  • Inline functions in FlatList/FlashList renderItem -- creates new function reference every render, defeats recycling/virtualization.
  • Adding key props to FlashList items -- breaks cell recycling, the core performance advantage of FlashList.
  • Running heavy computation synchronously during screen transitions -- blocks the JS thread, causes transition jank.
  • Using console.log in production bundles -- causes JS thread bottlenecks. Use babel-plugin-transform-remove-console.

Medium Priority Issues:

  • Adding manual useMemo/useCallback everywhere without profiling -- adds code complexity, may be redundant with React Compiler.
  • Using ScrollView + map() for lists with 50+ items -- no virtualization, all items rendered in memory simultaneously.
  • Inline style objects in frequently re-rendering components -- creates new object reference every render.
  • Not providing getItemLayout for fixed-height FlatList items -- forces measurement on every scroll, missing a significant optimization.
  • Namespace imports (import * as) for large libraries -- prevents tree shaking, inflates bundle.

Gotchas & Edge Cases:

  • removeClippedSubviews helps memory on Android but can cause blank areas on iOS -- use Platform.OS === "android" guard.
  • useNativeDriver: true only supports non-layout properties (transform, opacity) -- width, height, padding animations must run on JS thread.
  • FlatList onEndReached fires immediately if initial data fits the screen -- set onEndReachedThreshold carefully and guard against duplicate calls.
  • Hermes heap snapshots show retained objects including JS engine internals -- filter for your app's classes/closures when analyzing.
  • React Compiler cannot optimize components that use arguments, eval, or non-standard patterns -- these fall back to uncompiled behavior.
  • LayoutAnimation affects ALL layout changes in the next cycle, not just the one you intended -- scope it carefully or use the Animated API for targeted animations.
  • InteractionManager.runAfterInteractions tasks are canceled if the component unmounts -- always clean up with task.cancel() in useEffect return.
  • FlashList v2 requires New Architecture -- use FlashList v1 or FlatList if on legacy architecture.
  • Dev mode "Perf Monitor" shows in-app FPS but includes dev overhead -- only trust release build measurements.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST profile BEFORE optimizing -- use React Native DevTools Profiler or platform tools to identify actual bottlenecks, never optimize blindly)

(You MUST test performance in RELEASE builds -- dev mode adds significant overhead that masks real performance characteristics)

(You MUST understand the JS thread vs UI thread distinction -- animations belong on the UI thread, heavy computation must be deferred with InteractionManager)

(You MUST memoize renderItem callbacks and item components for FlashList/FlatList -- inline functions break virtualization performance)

(You MUST check if React Compiler is enabled before adding manual useMemo/useCallback/React.memo -- the compiler auto-memoizes and manual hints become redundant)

Failure to follow these rules will result in blind optimization that misses real bottlenecks, jank during transitions, and unnecessary code complexity.

</critical_reminders>

Files (skills)
  • examples
    • core.md 16.2 KB
      # React Native Performance - Core Optimization Patterns
      
      > Re-render prevention, list optimization, deferred work, and image performance. See [profiling.md](profiling.md) for profiling tools and memory analysis. See [SKILL.md](../SKILL.md) for decision frameworks and red flags.
      
      **Prerequisites**: Understand React rendering model (when components re-render) and React Native's threading model (JS thread vs UI thread).
      
      ---
      
      ## Pattern 1: Deferred Work with InteractionManager
      
      Defer heavy computation until after animations/transitions finish. This keeps navigation smooth at 60 FPS.
      
      ```typescript
      import { useEffect, useState } from "react";
      import {
        InteractionManager,
        View,
        Text,
        ActivityIndicator,
        StyleSheet,
      } from "react-native";
      
      interface DeferredScreenProps {
        userId: string;
      }
      
      // Defer data processing until transition completes
      export function DeferredScreen({ userId }: DeferredScreenProps) {
        const [data, setData] = useState<ProcessedData | null>(null);
        const [isReady, setIsReady] = useState(false);
      
        useEffect(() => {
          // runAfterInteractions waits for all animations to complete
          const task = InteractionManager.runAfterInteractions(async () => {
            const raw = await fetchUserData(userId);
            const processed = processData(raw); // Heavy computation
            setData(processed);
            setIsReady(true);
          });
      
          // CRITICAL: Cancel if component unmounts during transition
          return () => task.cancel();
        }, [userId]);
      
        if (!isReady) {
          return (
            <View style={styles.loader}>
              <ActivityIndicator size="large" color="#007AFF" />
            </View>
          );
        }
      
        return (
          <View style={styles.container}>
            <Text style={styles.title}>{data?.title}</Text>
          </View>
        );
      }
      
      const styles = StyleSheet.create({
        loader: {
          flex: 1,
          justifyContent: "center",
          alignItems: "center",
        },
        container: {
          flex: 1,
          padding: 16,
        },
        title: {
          fontSize: 24,
          fontWeight: "600",
        },
      });
      ```
      
      **Why good:** Navigation transition runs at 60 FPS because heavy work is deferred. Cleanup prevents memory leaks if user navigates away before work completes.
      
      ---
      
      ### Touch Response Optimization with requestAnimationFrame
      
      When `onPress` handlers do heavy work, the touch feedback (opacity change, ripple) is delayed. Wrap expensive work in `requestAnimationFrame` to let the visual feedback render first.
      
      ```typescript
      import { useCallback } from "react";
      import { Pressable, Text } from "react-native";
      
      interface ActionButtonProps {
        onAction: () => void;
        label: string;
      }
      
      export function ActionButton({ onAction, label }: ActionButtonProps) {
        const handlePress = useCallback(() => {
          // Let the press animation render first, then do heavy work
          requestAnimationFrame(() => {
            onAction();
          });
        }, [onAction]);
      
        return (
          <Pressable onPress={handlePress}>
            <Text>{label}</Text>
          </Pressable>
        );
      }
      ```
      
      **Why good:** Touch feedback appears immediately because `requestAnimationFrame` defers the heavy work to the next frame after the UI update.
      
      ---
      
      ## Pattern 2: Re-Render Optimization
      
      ### Memoized List Items
      
      The most impactful optimization: prevent list items from re-rendering when the parent re-renders.
      
      ```typescript
      import { memo, useCallback, type ReactNode } from "react";
      import { View, Text, Pressable, Image, StyleSheet } from "react-native";
      
      const IMAGE_SIZE = 60;
      
      interface Product {
        id: string;
        name: string;
        price: number;
        imageUrl: string;
      }
      
      interface ProductItemProps {
        item: Product;
        onPress: (id: string) => void;
      }
      
      // React.memo prevents re-render when props haven't changed
      const ProductItem = memo(function ProductItem({
        item,
        onPress,
      }: ProductItemProps) {
        // useCallback gives stable reference -- critical for memo to work
        const handlePress = useCallback(() => {
          onPress(item.id);
        }, [item.id, onPress]);
      
        return (
          <Pressable onPress={handlePress} style={styles.item}>
            <Image
              source={{ uri: item.imageUrl }}
              style={styles.image}
              resizeMode="cover"
            />
            <View style={styles.details}>
              <Text style={styles.name} numberOfLines={1}>
                {item.name}
              </Text>
              <Text style={styles.price}>${item.price.toFixed(2)}</Text>
            </View>
          </Pressable>
        );
      });
      
      export { ProductItem };
      
      const styles = StyleSheet.create({
        item: {
          height: 80,
          flexDirection: "row",
          alignItems: "center",
          paddingHorizontal: 16,
          backgroundColor: "#FFFFFF",
        },
        image: {
          width: IMAGE_SIZE,
          height: IMAGE_SIZE,
          borderRadius: 8,
          backgroundColor: "#F2F2F7",
        },
        details: {
          flex: 1,
          marginLeft: 12,
        },
        name: {
          fontSize: 16,
          fontWeight: "600",
          color: "#1A1A1A",
        },
        price: {
          fontSize: 14,
          color: "#007AFF",
          marginTop: 4,
        },
      });
      ```
      
      **Why good:** memo prevents re-rendering when parent state changes but item props are identical. useCallback ensures the onPress reference is stable between parent renders.
      
      ---
      
      ### Custom Comparison Function
      
      For complex props where shallow comparison is too expensive or too broad:
      
      ```typescript
      interface ExpensiveItemProps {
        data: ComplexData;
        config: RenderConfig;
        onSelect: (id: string) => void;
      }
      
      const ExpensiveItem = memo(
        function ExpensiveItem({ data, config, onSelect }: ExpensiveItemProps) {
          return (
            <View>
              <Text>{data.title}</Text>
            </View>
          );
        },
        // Custom comparator: only re-render when meaningful fields change
        (prevProps, nextProps) => {
          return (
            prevProps.data.id === nextProps.data.id &&
            prevProps.data.updatedAt === nextProps.data.updatedAt &&
            prevProps.config.mode === nextProps.config.mode
          );
        },
      );
      
      export { ExpensiveItem };
      ```
      
      **Why good:** Custom comparator avoids expensive deep comparison while still catching meaningful changes. Only checks fields that affect rendering.
      
      **When to use:** When the default shallow comparison is too broad (re-renders on irrelevant prop changes) or the data object is deeply nested.
      
      ---
      
      ### Expensive Computation with useMemo
      
      ```typescript
      import { useMemo } from "react";
      
      interface DataProcessorProps {
        items: Item[];
        filters: Filters;
        sortOrder: "asc" | "desc";
      }
      
      export function DataProcessor({ items, filters, sortOrder }: DataProcessorProps) {
        // Only recompute when inputs change
        const processedData = useMemo(() => {
          return items
            .filter((item) => matchesFilters(item, filters))
            .sort((a, b) => {
              const comparison = a.name.localeCompare(b.name);
              return sortOrder === "asc" ? comparison : -comparison;
            });
        }, [items, filters, sortOrder]);
      
        return <ItemList data={processedData} />;
      }
      
      function matchesFilters(item: Item, filters: Filters): boolean {
        if (filters.category && item.category !== filters.category) return false;
        if (filters.minPrice && item.price < filters.minPrice) return false;
        if (filters.maxPrice && item.price > filters.maxPrice) return false;
        return true;
      }
      ```
      
      **Why good:** useMemo prevents re-running filter+sort on every render. The computation only runs when items, filters, or sortOrder actually change.
      
      **When to skip:** If the computation is trivial (single property access, simple boolean check), useMemo adds overhead without benefit. Profile first.
      
      ---
      
      ## Pattern 3: FlashList Optimization
      
      FlashList v2 uses cell recycling (reuses component instances) for superior performance. Key rules: memoize renderItem, never add `key` props to items, use `getItemType` for heterogeneous lists.
      
      ```typescript
      import { FlashList } from "@shopify/flash-list";
      import { useCallback } from "react";
      
      const ITEM_HEIGHT = 80;
      
      interface ProductListProps {
        products: Product[];
        onProductPress: (id: string) => void;
        onEndReached?: () => void;
      }
      
      export function OptimizedFlashList({
        products,
        onProductPress,
        onEndReached,
      }: ProductListProps) {
        // Stable renderItem reference
        const renderItem = useCallback(
          ({ item }: { item: Product }) => (
            <ProductItem item={item} onPress={onProductPress} />
          ),
          [onProductPress],
        );
      
        // getItemType improves recycling -- items of same type share a pool
        const getItemType = useCallback((item: Product) => {
          return item.category;
        }, []);
      
        return (
          <FlashList
            data={products}
            renderItem={renderItem}
            // FlashList v2: estimatedItemSize is OPTIONAL (auto-measures)
            // Providing it still helps initial render before measurements
            estimatedItemSize={ITEM_HEIGHT}
            getItemType={getItemType}
            onEndReached={onEndReached}
            onEndReachedThreshold={0.5}
            showsVerticalScrollIndicator={false}
          />
        );
      }
      ```
      
      **Why good:** useCallback gives stable renderItem (prevents re-creating the function), getItemType groups items by category for efficient recycling pools, estimatedItemSize helps the initial layout pass.
      
      **Critical FlashList rules:**
      
      - Do NOT add `key` props to rendered items -- breaks cell recycling
      - DO memoize the item component with React.memo
      - DO use `getItemType` when items have different layouts
      - FlashList v2 requires New Architecture -- use v1 or FlatList on legacy
      
      ---
      
      ## Pattern 4: FlatList Tuning
      
      When using FlatList, tuning props significantly impacts scrolling performance.
      
      ```typescript
      import { useCallback, useMemo } from "react";
      import {
        FlatList,
        Platform,
        type ListRenderItem,
      } from "react-native";
      
      // Constants for tuning -- adjust based on profiling
      const ITEM_HEIGHT = 80;
      const SEPARATOR_HEIGHT = 1;
      const WINDOW_SIZE = 5; // Number of viewport heights to render
      const MAX_TO_RENDER_PER_BATCH = 10; // Items per render batch
      const INITIAL_NUM_TO_RENDER = 10; // Items on first render
      const ON_END_REACHED_THRESHOLD = 0.5;
      
      export function TunedFlatList({
        products,
        onProductPress,
        onEndReached,
        isLoadingMore = false,
      }: ProductListProps & { isLoadingMore?: boolean }) {
        const renderItem: ListRenderItem<Product> = useCallback(
          ({ item }) => <ProductItem item={item} onPress={onProductPress} />,
          [onProductPress],
        );
      
        const keyExtractor = useCallback((item: Product) => item.id, []);
      
        // getItemLayout for fixed-height items -- MAJOR performance win
        // Enables instant scrollToIndex and eliminates measurement overhead
        const getItemLayout = useCallback(
          (_data: Product[] | null | undefined, index: number) => ({
            length: ITEM_HEIGHT + SEPARATOR_HEIGHT,
            offset: (ITEM_HEIGHT + SEPARATOR_HEIGHT) * index,
            index,
          }),
          [],
        );
      
        // Memoize extraData to prevent unnecessary re-renders
        const extraData = useMemo(
          () => ({ isLoadingMore }),
          [isLoadingMore],
        );
      
        return (
          <FlatList
            data={products}
            renderItem={renderItem}
            keyExtractor={keyExtractor}
            getItemLayout={getItemLayout}
            extraData={extraData}
            windowSize={WINDOW_SIZE}
            maxToRenderPerBatch={MAX_TO_RENDER_PER_BATCH}
            initialNumToRender={INITIAL_NUM_TO_RENDER}
            onEndReached={onEndReached}
            onEndReachedThreshold={ON_END_REACHED_THRESHOLD}
            // removeClippedSubviews reclaims memory on Android
            // but can cause blank areas on iOS -- platform-gate it
            removeClippedSubviews={Platform.OS === "android"}
            showsVerticalScrollIndicator={false}
          />
        );
      }
      ```
      
      **Key tuning props explained:**
      
      | Prop                    | Default | Effect                                                                        |
      | ----------------------- | ------- | ----------------------------------------------------------------------------- |
      | `windowSize`            | 21      | Viewports to keep rendered. Lower = less memory, more blank area. Start at 5. |
      | `maxToRenderPerBatch`   | 10      | Items per render cycle. Higher = fewer blanks, more JS thread work.           |
      | `initialNumToRender`    | 10      | Items on first render. Match to visible items for fastest initial paint.      |
      | `getItemLayout`         | None    | Eliminates measurement for fixed-height items. Massive scrollToIndex speedup. |
      | `removeClippedSubviews` | false   | Reclaims memory for off-screen items. Android only -- causes issues on iOS.   |
      
      ---
      
      ## Pattern 5: Anti-Patterns to Avoid
      
      ### Inline Styles in Frequently Rendered Components
      
      ```typescript
      // BAD: New style object created every render
      function BadItem({ color }: { color: string }) {
        return (
          <View style={{ padding: 16, backgroundColor: color }}>
            <Text style={{ fontSize: 16, color: "#000" }}>Item</Text>
          </View>
        );
      }
      
      // GOOD: Static styles in StyleSheet, dynamic styles via useMemo or array
      const styles = StyleSheet.create({
        container: { padding: 16 },
        text: { fontSize: 16, color: "#000" },
      });
      
      function GoodItem({ color }: { color: string }) {
        return (
          <View style={[styles.container, { backgroundColor: color }]}>
            <Text style={styles.text}>Item</Text>
          </View>
        );
      }
      ```
      
      **Why:** StyleSheet.create optimizes styles by sending them to native once. Inline objects are re-created every render and compared by reference.
      
      ---
      
      ### Unstable extraData and Callbacks
      
      ```typescript
      // BAD: New array reference every render -- triggers full list re-render
      <FlatList
        data={items}
        extraData={[selectedId, sortOrder]}
        renderItem={({ item }) => <Item item={item} />}
      />
      
      // GOOD: Memoized extraData + stable renderItem
      const extraData = useMemo(
        () => ({ selectedId, sortOrder }),
        [selectedId, sortOrder],
      );
      
      const renderItem = useCallback(
        ({ item }: { item: ItemType }) => <MemoizedItem item={item} />,
        [],
      );
      
      <FlatList data={items} extraData={extraData} renderItem={renderItem} />
      ```
      
      **Why:** FlatList uses referential equality to decide whether to re-render. New array/object references trigger a full re-render of all visible items.
      
      ---
      
      ## Pattern 6: Native Animation Offloading
      
      Keep animations on the UI thread so they run at 60 FPS even when the JS thread is busy.
      
      ```typescript
      import { Animated, Easing } from "react-native";
      
      const ANIMATION_DURATION_MS = 300;
      
      // GOOD: useNativeDriver offloads to UI thread
      const fadeIn = (animatedValue: Animated.Value) => {
        Animated.timing(animatedValue, {
          toValue: 1,
          duration: ANIMATION_DURATION_MS,
          easing: Easing.inOut(Easing.ease),
          useNativeDriver: true, // Runs on UI thread
        }).start();
      };
      
      // GOOD: LayoutAnimation for simple layout transitions
      import { LayoutAnimation, UIManager, Platform } from "react-native";
      
      // Enable on Android (enabled by default on iOS)
      if (Platform.OS === "android") {
        UIManager.setLayoutAnimationEnabledExperimental?.(true);
      }
      
      function toggleExpanded() {
        // Animate the next layout change -- fire-and-forget
        LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
        setExpanded((prev) => !prev);
      }
      ```
      
      **Why good:** `useNativeDriver: true` sends the animation description to native once and the UI thread runs it independently of JS. LayoutAnimation uses Core Animation (iOS) and bypasses both JS and UI thread frame drops.
      
      **Limitation:** `useNativeDriver` only supports non-layout properties (transform, opacity). Width, height, padding, margin animations must run on the JS thread or use LayoutAnimation.
      
      ---
      
      ## Pattern 7: Image Performance Strategies
      
      ### Right-Sizing Images
      
      ```typescript
      import { Image, PixelRatio, Dimensions } from "react-native";
      
      const SCREEN_WIDTH = Dimensions.get("window").width;
      const PIXEL_RATIO = PixelRatio.get();
      
      // Calculate the actual pixel dimensions needed
      const THUMBNAIL_DISPLAY_SIZE = 80;
      const THUMBNAIL_PIXEL_SIZE = THUMBNAIL_DISPLAY_SIZE * PIXEL_RATIO;
      
      // Request appropriately sized images from your CDN/server
      const thumbnailUrl = `${baseUrl}?w=${THUMBNAIL_PIXEL_SIZE}&h=${THUMBNAIL_PIXEL_SIZE}`;
      
      // Full-width hero image
      const HERO_PIXEL_WIDTH = SCREEN_WIDTH * PIXEL_RATIO;
      const heroUrl = `${baseUrl}?w=${HERO_PIXEL_WIDTH}`;
      ```
      
      **Why good:** Requesting images at the exact pixel size needed avoids downloading 4K images for 80px thumbnails. Reduces download time, memory usage, and decode time.
      
      ### Preloading Critical Images
      
      ```typescript
      import { Image } from "react-native";
      
      // Preload images that will be needed soon
      export function preloadCriticalImages(imageUrls: string[]) {
        imageUrls.forEach((url) => {
          Image.prefetch(url);
        });
      }
      
      // Call before navigating to image-heavy screen
      function handleNavigateToGallery() {
        preloadCriticalImages(galleryImageUrls.slice(0, 5)); // Preload first 5
        navigation.navigate("Gallery", { images: galleryImageUrls });
      }
      ```
      
      **Why good:** Images start downloading before they're visible, eliminating the delay when the user actually sees them.
      
    • profiling.md 13 KB
      # React Native Performance - Profiling & Monitoring
      
      > Profiling tools, memory analysis, bundle inspection, and production monitoring. See [core.md](core.md) for optimization patterns. See [SKILL.md](../SKILL.md) for decision frameworks.
      
      **Prerequisites**: Understand React Native's threading model (JS thread vs UI thread) and basics of profiling tools.
      
      ---
      
      ## Pattern 1: React Native DevTools Profiler
      
      React Native DevTools (default since 0.76) replaces Flipper for JavaScript-level profiling. It includes React Profiler for component render analysis and Performance panel for JS execution traces.
      
      ### Launching DevTools
      
      ```bash
      # DevTools opens automatically when running Metro
      npx react-native start
      
      # Or open manually via the Dev Menu:
      # Shake device / Cmd+D (iOS) / Cmd+M (Android emulator)
      # Select "Open DevTools"
      ```
      
      ### React Profiler Workflow
      
      1. **Open the Profiler tab** in React Native DevTools
      2. **Click Record** and interact with the feature you want to profile
      3. **Click Stop** to analyze the recording
      4. **Read the flame graph** -- taller bars = more render time, gray bars = components that didn't re-render
      
      **Key metrics to look for:**
      
      | Metric                          | Target            | Action if exceeded                    |
      | ------------------------------- | ----------------- | ------------------------------------- |
      | Component render time           | < 16ms            | Memoize, simplify, or split component |
      | Re-render count per interaction | Minimal           | Add React.memo, check prop stability  |
      | Commit frequency                | 1 per interaction | Check for cascading state updates     |
      
      ### Highlight Re-Renders
      
      ```
      Settings (gear icon) -> Check "Highlight updates when components render"
      ```
      
      Components that re-render flash with colored borders. If the entire screen flashes when a single input changes, you have a re-render propagation problem.
      
      ---
      
      ## Pattern 2: Performance Monitor (In-App FPS)
      
      The built-in Perf Monitor shows real-time FPS for both JS and UI threads.
      
      ```
      # Enable via Dev Menu:
      Shake device -> "Show Perf Monitor"
      
      # Or programmatically in development:
      if (__DEV__) {
        // The perf monitor overlay appears showing:
        // - JS thread FPS (target: 60)
        // - UI thread FPS (target: 60)
        // - RAM usage
        // - Views count
      }
      ```
      
      **Reading the Perf Monitor:**
      
      | Indicator       | Meaning                   | Action                                    |
      | --------------- | ------------------------- | ----------------------------------------- |
      | JS FPS < 55     | JS thread congested       | Defer work, reduce re-renders             |
      | UI FPS < 55     | UI thread congested       | Offload to native driver, simplify layout |
      | Both low        | General overload          | Profile to find root cause                |
      | JS low, UI fine | Computation bottleneck    | InteractionManager, useMemo, workers      |
      | JS fine, UI low | Layout/drawing bottleneck | Simplify views, reduce nesting            |
      
      **Warning:** Perf Monitor shows dev-mode FPS which includes dev overhead. Only trust release build measurements for accurate profiling.
      
      ---
      
      ## Pattern 3: Hermes CPU Profiling
      
      Hermes profiles show exactly which functions consume CPU time. Use this to identify expensive functions and hot code paths.
      
      ### Recording a Hermes Profile
      
      ```
      # Via Dev Menu:
      Shake device -> "Start/Stop Sampling Profiler"
      
      # The profile is saved to a file on device.
      # Pull it and open in Chrome DevTools (chrome://tracing)
      # or in React Native DevTools Performance panel (0.83+)
      ```
      
      ### What to Look For
      
      ```
      Recording workflow:
      1. Start profile
      2. Reproduce the slow interaction
      3. Stop profile
      4. Analyze in chrome://tracing or DevTools Performance panel
      
      Key observations:
      - Functions consuming > 10% of total time -> optimize or defer
      - Long synchronous blocks on JS thread -> break into chunks
      - Frequent GC pauses -> check for excessive object allocation
      - Bridge calls (legacy arch) -> consider JSI migration
      ```
      
      **Hermes-specific considerations:**
      
      - Hermes compiles JS to bytecode at build time -- startup is fast but some runtime optimizations differ from V8/JSC
      - Hermes GC is incremental and concurrent -- large allocations still cause pauses
      - Profile in release mode for accurate timings (dev mode adds instrumentation)
      
      ---
      
      ## Pattern 4: Platform-Specific Profilers
      
      ### iOS: Xcode Instruments
      
      ```
      Xcode -> Product -> Profile (Cmd+I)
      
      Key instruments for React Native:
      - Time Profiler: CPU usage by function (covers both native and JS)
      - Allocations: Memory allocation patterns, leak detection
      - Core Animation (Graphics): FPS, GPU utilization, off-screen rendering
      - Network: Network request timing and size
      - Energy Log: Battery impact of background operations
      ```
      
      **Core Animation instrument checklist:**
      
      - [ ] Color Blended Layers -- red areas = expensive alpha compositing (fix: opaque backgrounds)
      - [ ] Color Off-screen Rendered -- yellow areas = offscreen rendering (fix: reduce shadows, masks, corner radius)
      - [ ] Hit Testing -- verify touch targets are efficient
      
      ### Android: Android Studio Profiler
      
      ```
      Android Studio -> View -> Tool Windows -> Profiler
      
      Key profiler views:
      - CPU: Method traces, flame charts, thread activity
      - Memory: Heap dump, allocation tracking, GC events
      - Network: Request timeline with payload sizes
      - Energy: Wake locks, alarms, battery drain
      
      Connect to running app:
      1. Run app in debug mode
      2. Open Profiler in Android Studio
      3. Select your app process
      4. Record the specific interaction
      ```
      
      **Android-specific tips:**
      
      - Use systrace for low-level frame analysis: `npx react-native systrace`
      - Check GPU rendering with "Profile HWUI Rendering" in Developer Options
      - Green bars = within 16ms budget, red bars = dropped frames
      
      ---
      
      ## Pattern 5: Memory Leak Detection
      
      ### Heap Snapshot Workflow
      
      Use React Native DevTools Memory panel (0.83+) or Chrome DevTools to capture heap snapshots.
      
      ```
      Heap snapshot workflow:
      1. Navigate to the suspect screen
      2. Take Snapshot A (baseline)
      3. Perform the action (navigate away and back, open/close modal, etc.)
      4. Force garbage collection (click the trash icon in DevTools)
      5. Take Snapshot B
      6. Compare A and B -- look for objects that should have been freed
      
      Common leak indicators:
      - Growing "Detached" DOM nodes (Fiber nodes in RN)
      - Event listener count increasing over time
      - Closure references holding large data structures
      - Timer handles accumulating without cleanup
      ```
      
      ### Common Memory Leak Patterns
      
      ```typescript
      // LEAK: Timer not cleared on unmount
      useEffect(() => {
        const timer = setInterval(refresh, POLL_INTERVAL_MS);
        // Missing: return () => clearInterval(timer);
      }, []);
      
      // LEAK: Subscription not removed
      useEffect(() => {
        const sub = eventEmitter.addListener("event", handler);
        // Missing: return () => sub.remove();
      }, []);
      
      // LEAK: Async callback updates state after unmount
      useEffect(() => {
        fetchData().then((data) => setState(data));
        // Missing: mounted flag to prevent state update after unmount
      }, []);
      
      // FIXED: All three patterns with proper cleanup
      useEffect(() => {
        let isMounted = true;
        const timer = setInterval(refresh, POLL_INTERVAL_MS);
        const sub = eventEmitter.addListener("event", handler);
      
        fetchData().then((data) => {
          if (isMounted) setState(data);
        });
      
        return () => {
          isMounted = false;
          clearInterval(timer);
          sub.remove();
        };
      }, []);
      ```
      
      ### Platform-Specific Leak Detection
      
      ```
      iOS:
      - Xcode Memory Graph Debugger: Debug -> Debug Memory Graph
        Shows all live objects and their retain chains
        Purple markers indicate potential leaks
      - Instruments Allocations: Track allocation growth over time
      
      Android:
      - LeakCanary: Automatic detection of Activity/Fragment leaks
        Notifies when objects that should be GC'd are retained
      - Android Studio Heap Dump: Capture and analyze live heap
        Filter by package name to find your app's retained objects
      ```
      
      ---
      
      ## Pattern 6: Bundle Size Analysis
      
      ### Visualizing the Bundle
      
      ```bash
      # Install the bundle visualizer
      npx react-native-bundle-visualizer
      
      # Generates an interactive treemap showing:
      # - Total bundle size
      # - Per-module size contribution
      # - Dependency tree visualization
      ```
      
      ### Bundle Size Reduction Strategies
      
      ```typescript
      // GOOD: Named imports for tree shaking
      import { format, parseISO } from "date-fns";
      
      // BAD: Imports entire library
      import * as dateFns from "date-fns";
      import _ from "lodash"; // Pulls in entire lodash (~70KB)
      
      // GOOD: Import specific lodash functions
      import debounce from "lodash/debounce"; // Only debounce (~1KB)
      ```
      
      ### Production Build Optimization
      
      ```javascript
      // babel.config.js -- remove console.log in production
      module.exports = {
        presets: ["module:@react-native/babel-preset"],
        env: {
          production: {
            plugins: ["transform-remove-console"],
          },
        },
      };
      ```
      
      ### Platform-Specific Code Splitting
      
      ```
      // Use platform extensions to avoid shipping irrelevant code
      component.ios.tsx    -- iOS-only implementation
      component.android.tsx -- Android-only implementation
      
      // Metro resolves the correct file per platform at build time
      // The other platform's code is NOT included in the bundle
      import { Component } from "./component"; // Resolves to .ios.tsx or .android.tsx
      ```
      
      ---
      
      ## Pattern 7: Performance Monitoring Hook
      
      A development-only hook to detect excessive re-renders. Useful during development to catch performance regressions early.
      
      ```typescript
      import { useEffect, useRef } from "react";
      
      const RENDER_THRESHOLD = 10;
      const RAPID_RENDER_MS = 16; // One frame at 60fps
      
      export function useRenderMonitor(componentName: string) {
        const renderCount = useRef(0);
        const lastRenderTime = useRef(Date.now());
      
        useEffect(() => {
          if (!__DEV__) return;
      
          renderCount.current += 1;
          const now = Date.now();
          const elapsed = now - lastRenderTime.current;
          lastRenderTime.current = now;
      
          if (renderCount.current > RENDER_THRESHOLD) {
            console.warn(
              `[Perf] ${componentName}: ${renderCount.current} renders`,
            );
          }
      
          if (elapsed < RAPID_RENDER_MS && renderCount.current > 1) {
            console.warn(
              `[Perf] ${componentName}: rapid re-render (${elapsed}ms gap)`,
            );
          }
        });
      
        // Log total on unmount
        useEffect(() => {
          return () => {
            if (__DEV__ && renderCount.current > RENDER_THRESHOLD) {
              console.log(
                `[Perf] ${componentName} total renders: ${renderCount.current}`,
              );
            }
          };
        }, [componentName]);
      }
      
      // Usage: Add to suspect components during debugging
      function SuspectComponent() {
        useRenderMonitor("SuspectComponent");
        return <View>{/* ... */}</View>;
      }
      ```
      
      **Why good:** Catches excessive re-renders during development without production overhead (`__DEV__` guard). Rapid re-render detection catches cascading state updates.
      
      **When to use:** During development to diagnose re-render problems. Remove or leave in place (the `__DEV__` guard ensures zero production cost).
      
      ---
      
      ## Pattern 8: Production Performance Monitoring
      
      For production apps, use a monitoring SDK to capture real-user performance data. Generic pattern for any monitoring solution:
      
      ```typescript
      import { AppState, type AppStateStatus } from "react-native";
      
      const COLD_START_THRESHOLD_MS = 3000;
      const SCREEN_RENDER_THRESHOLD_MS = 500;
      
      // Track app startup time
      export function measureStartupTime() {
        const startTime = global.__APP_START_TIME ?? Date.now();
        const endTime = Date.now();
        const startupMs = endTime - startTime;
      
        if (startupMs > COLD_START_THRESHOLD_MS) {
          // Report slow startup to your monitoring solution
          reportMetric("cold_start_ms", startupMs);
        }
      }
      
      // Track screen render time
      export function useScreenRenderTime(screenName: string) {
        const mountTime = useRef(Date.now());
      
        useEffect(() => {
          const renderTime = Date.now() - mountTime.current;
          reportMetric("screen_render_ms", renderTime, { screen: screenName });
      
          if (renderTime > SCREEN_RENDER_THRESHOLD_MS) {
            reportMetric("slow_screen_render", renderTime, { screen: screenName });
          }
        }, [screenName]);
      }
      
      // Track app state changes (background/foreground)
      export function useAppStateMonitoring() {
        const lastBackgroundTime = useRef<number | null>(null);
      
        useEffect(() => {
          const handleAppStateChange = (nextState: AppStateStatus) => {
            if (nextState === "background") {
              lastBackgroundTime.current = Date.now();
            } else if (nextState === "active" && lastBackgroundTime.current) {
              const bgDuration = Date.now() - lastBackgroundTime.current;
              reportMetric("background_duration_ms", bgDuration);
              lastBackgroundTime.current = null;
            }
          };
      
          const subscription = AppState.addEventListener(
            "change",
            handleAppStateChange,
          );
          return () => subscription.remove();
        }, []);
      }
      ```
      
      **Key production metrics to track:**
      
      | Metric              | Target  | Concern Threshold |
      | ------------------- | ------- | ----------------- |
      | Cold start time     | < 2s    | > 3s              |
      | Screen render time  | < 300ms | > 500ms           |
      | JS thread FPS       | 60      | < 50              |
      | Memory usage        | Stable  | Growing over time |
      | Crash-free sessions | > 99.5% | < 99%             |
      
  • reference.md 9.3 KB
    # React Native Performance Reference
    
    > Decision frameworks, checklists, performance targets, and quick reference. See [SKILL.md](SKILL.md) for red flags and anti-patterns.
    
    ---
    
    ## Performance Targets
    
    | Metric                    | Target  | Acceptable | Action Required                 |
    | ------------------------- | ------- | ---------- | ------------------------------- |
    | JS thread FPS             | 60      | 55+        | < 50: investigate JS bottleneck |
    | UI thread FPS             | 60      | 55+        | < 50: offload to native driver  |
    | Cold start time           | < 1.5s  | < 3s       | > 3s: lazy load, defer init     |
    | Screen render time        | < 200ms | < 500ms    | > 500ms: profile component tree |
    | List scroll FPS           | 60      | 55+        | < 50: FlashList, memoize items  |
    | Memory growth             | Stable  | < 5MB/min  | Growing: check for leaks        |
    | Bundle size (JS)          | < 3MB   | < 5MB      | > 5MB: analyze dependencies     |
    | TTI (Time to Interactive) | < 2s    | < 4s       | > 4s: defer non-critical work   |
    
    ---
    
    ## Decision Frameworks
    
    ### Optimization Decision Tree
    
    ```
    Is there a measurable performance problem?
    |-- NO -> Don't optimize. Ship it.
    +-- YES -> Have you profiled in a RELEASE build?
        |-- NO -> Profile first. Dev mode distorts results.
        +-- YES -> What does profiling show?
            |
            |-- JS thread < 55 FPS
            |   |-- During animation -> Offload animation to UI thread
            |   |-- During transition -> InteractionManager.runAfterInteractions
            |   |-- During list scroll -> Memoize renderItem, FlashList
            |   +-- During computation -> useMemo, break into chunks
            |
            |-- UI thread < 55 FPS
            |   |-- Complex shadows/borders -> Simplify, use elevation on Android
            |   |-- Deep view hierarchy -> Flatten nesting
            |   |-- Large images -> Right-size, cache
            |   +-- Off-screen rendering -> Check with Core Animation instrument
            |
            |-- Slow startup (> 3s)
            |   |-- Large bundle -> Named imports, remove unused deps
            |   |-- Heavy init -> Defer with InteractionManager
            |   |-- Many screens -> Lazy load non-critical screens
            |   +-- Hermes disabled -> Enable Hermes (default since 0.70)
            |
            |-- High memory (growing)
            |   |-- After navigation -> Check cleanup in useEffect
            |   |-- During scroll -> Check image sizes, removeClippedSubviews
            |   |-- Over time -> Heap snapshot comparison
            |   +-- Sudden spikes -> Large data processing, use pagination
            |
            +-- Large bundle (> 5MB JS)
                |-- One large dependency -> Find lighter alternative
                |-- Many unused exports -> Named imports, tree shaking
                |-- Platform-irrelevant code -> Use .ios.tsx/.android.tsx files
                +-- Dev-only code in production -> babel-plugin-transform-remove-console
    ```
    
    ### List Component Decision
    
    ```
    How many items?
    |-- < 10 -> ScrollView + map() is fine
    |-- 10-50 -> FlashList or FlatList recommended
    |-- 50-500 -> FlashList strongly recommended
    +-- 500+ -> FlashList v2 required for good performance
    
    On New Architecture (0.76+)?
    |-- YES -> FlashList v2 (cell recycling, auto-sizing)
    +-- NO -> FlashList v1 or FlatList
    
    Items have fixed height?
    |-- YES -> FlatList with getItemLayout (eliminates measurement)
    +-- NO -> FlashList v2 (handles dynamic heights automatically)
    ```
    
    ### Memoization Decision (with React Compiler awareness)
    
    ```
    Is React Compiler enabled?
    |-- YES -> Check DevTools for "Memo" badge
    |   |-- Badge present -> Don't add manual memoization
    |   +-- Badge absent -> Profile first, then consider manual memo
    +-- NO -> Is this a list item component?
        |-- YES -> Always: React.memo + useCallback for onPress
        +-- NO -> Does profiling show unnecessary re-renders?
            |-- YES -> Add React.memo to the component
            |   +-- Props include callbacks? -> useCallback in parent
            +-- NO -> Don't memoize (adds complexity without benefit)
    
    Is this an expensive computation?
    |-- Runs on every render? -> useMemo with correct deps
    |-- Runs once or rarely? -> No memoization needed
    +-- Not sure? -> Profile first. If < 1ms, skip memoization.
    ```
    
    ### Animation Thread Decision
    
    ```
    What type of animation?
    |-- Simple opacity/transform -> Animated with useNativeDriver: true
    |-- Layout change -> LayoutAnimation (bypasses JS thread entirely)
    |-- Gesture-driven -> Animation library worklets (UI thread)
    |-- Complex sequence -> Animation library (UI thread execution)
    +-- Layout properties (width, height) -> JS thread (useNativeDriver not supported)
    ```
    
    ---
    
    ## Performance Checklist
    
    ### Before Release
    
    - [ ] All performance testing done in RELEASE build (not dev)
    - [ ] No `console.log` in production (use `babel-plugin-transform-remove-console`)
    - [ ] FlatList/FlashList renderItem wrapped in useCallback
    - [ ] List item components wrapped in React.memo (unless React Compiler is active)
    - [ ] No inline styles in frequently re-rendering components
    - [ ] Animations use useNativeDriver where possible
    - [ ] Heavy initialization deferred with InteractionManager
    - [ ] No memory leaks (useEffect cleanup for timers, subscriptions, async)
    
    ### List Performance
    
    - [ ] Using FlashList or FlatList (not ScrollView + map) for 20+ items
    - [ ] renderItem wrapped in useCallback with minimal dependencies
    - [ ] Item components wrapped in React.memo
    - [ ] keyExtractor returns stable unique ID (not array index)
    - [ ] getItemLayout provided for fixed-height FlatList items
    - [ ] windowSize, maxToRenderPerBatch, initialNumToRender tuned
    - [ ] removeClippedSubviews gated to Android only
    - [ ] No `key` prop on FlashList items (breaks recycling)
    - [ ] getItemType provided for heterogeneous FlashList lists
    
    ### Image Performance
    
    - [ ] Images sized to display dimensions (not source dimensions)
    - [ ] PixelRatio considered for CDN image requests
    - [ ] Critical images preloaded before needed
    - [ ] Optimized image library used for image-heavy screens
    - [ ] resizeMode set appropriately (cover, contain, center)
    - [ ] Placeholder/loading states for network images
    
    ### Memory
    
    - [ ] All useEffect hooks have cleanup functions
    - [ ] Timers (setInterval, setTimeout) cleared on unmount
    - [ ] Event listeners removed on unmount
    - [ ] Async operations guarded against unmounted state updates
    - [ ] Large data sets paginated (not loaded all at once)
    - [ ] Heap snapshot comparison shows no leak growth
    
    ### Startup Time
    
    - [ ] Hermes enabled (default since 0.70)
    - [ ] Non-critical screens lazy-loaded
    - [ ] Heavy initialization deferred with InteractionManager
    - [ ] Named imports used (not namespace imports)
    - [ ] Unused dependencies removed
    - [ ] Bundle size analyzed and within targets
    
    ---
    
    ## Profiling Tools Quick Reference
    
    | Tool                             | What It Shows                            | When to Use                     |
    | -------------------------------- | ---------------------------------------- | ------------------------------- |
    | React Native DevTools Profiler   | Component render times, re-render counts | Diagnosing re-render issues     |
    | Perf Monitor (Dev Menu)          | Real-time JS/UI thread FPS               | Quick check during development  |
    | Hermes CPU Profile               | Function-level CPU time                  | Identifying expensive functions |
    | Xcode Instruments                | Native CPU, memory, GPU, energy          | iOS-specific deep profiling     |
    | Android Studio Profiler          | Native CPU, memory, network              | Android-specific deep profiling |
    | react-native-bundle-visualizer   | Bundle composition treemap               | Reducing bundle size            |
    | Heap Snapshots (DevTools Memory) | Object retention, memory leaks           | Diagnosing memory growth        |
    | Core Animation (Xcode)           | Blended layers, offscreen rendering      | iOS GPU optimization            |
    | systrace (Android)               | Low-level frame analysis                 | Android frame drop analysis     |
    
    ---
    
    ## Frame Budget Reference
    
    ```
    60 FPS = 16.67ms per frame
    
    Each frame must complete within budget on BOTH threads:
    
    JS Thread (16.67ms budget):
    ├── React reconciliation (diffing)
    ├── State updates and effects
    ├── Event handlers
    ├── API calls (scheduling, not waiting)
    └── Bridge communication (legacy) / JSI calls (new arch)
    
    UI Thread (16.67ms budget):
    ├── Layout calculation
    ├── View rendering
    ├── Native animations
    ├── Touch event handling
    └── Scroll handling
    ```
    
    ---
    
    ## Quick Performance Wins
    
    | Problem                   | Quick Fix                                   | Impact |
    | ------------------------- | ------------------------------------------- | ------ |
    | Jank during navigation    | `InteractionManager.runAfterInteractions()` | High   |
    | Slow FlatList scrolling   | `getItemLayout` for fixed-height items      | High   |
    | List items all re-render  | `React.memo` + `useCallback` for renderItem | High   |
    | Slow startup              | `lazy()` for non-critical screens           | Medium |
    | Large bundle              | Named imports + remove unused deps          | Medium |
    | Console.log in production | `babel-plugin-transform-remove-console`     | Medium |
    | Image jank in lists       | Right-size + preload + caching library      | Medium |
    | Sluggish touch response   | `requestAnimationFrame(() => heavyWork())`  | Low    |
    | Inline styles in lists    | `StyleSheet.create` + style arrays          | Low    |
    
  • SKILL.md 21.1 KB
    ---
    name: mobile-performance-react-native
    description: React Native performance profiling, optimization, and monitoring - JS/UI thread analysis, re-render prevention, list optimization, image performance, bundle size, startup time, memory leaks, React Compiler, New Architecture benefits
    ---
    
    # React Native Performance Patterns
    
    > **Quick Guide:** Profile before optimizing -- use React Native DevTools Profiler (replaces Flipper since 0.76) and platform profilers to find actual bottlenecks. Target 60 FPS (16.67ms per frame). Understand JS thread vs UI thread: animations on the UI thread, business logic on JS. Use React.memo + useCallback for list items, FlashList for large lists, InteractionManager to defer heavy work during transitions. React Compiler (v1.0+) auto-memoizes most components -- verify before adding manual memoization. Always test in release builds; dev mode adds significant overhead.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST profile BEFORE optimizing -- use React Native DevTools Profiler or platform tools to identify actual bottlenecks, never optimize blindly)**
    
    **(You MUST test performance in RELEASE builds -- dev mode adds significant overhead that masks real performance characteristics)**
    
    **(You MUST understand the JS thread vs UI thread distinction -- animations belong on the UI thread, heavy computation must be deferred with InteractionManager)**
    
    **(You MUST memoize renderItem callbacks and item components for FlashList/FlatList -- inline functions break virtualization performance)**
    
    **(You MUST check if React Compiler is enabled before adding manual useMemo/useCallback/React.memo -- the compiler auto-memoizes and manual hints become redundant)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** React Native performance, FPS, frame rate, JS thread, UI thread, re-render, React.memo, useMemo, useCallback, FlashList optimization, FlatList optimization, InteractionManager, requestAnimationFrame, Hermes, bytecode, bundle size, Metro, tree shaking, startup time, memory leak, heap snapshot, React Compiler, auto-memoization, react-native-performance, Flipper profiler, React Native DevTools, Perf Monitor, useNativeDriver, LayoutAnimation, Reanimated worklet
    
    **When to use:**
    
    - Diagnosing dropped frames, jank, or slow transitions
    - Optimizing list scrolling performance (FlashList/FlatList)
    - Reducing re-renders in component trees
    - Profiling JS thread vs UI thread bottlenecks
    - Reducing app startup time or bundle size
    - Detecting and fixing memory leaks
    - Deciding whether to add manual memoization vs relying on React Compiler
    - Monitoring performance in production
    
    **When NOT to use:**
    
    - General React Native component architecture (use the framework skill)
    - Navigation setup and patterns (use the framework skill)
    - Styling and theming patterns (use the framework skill)
    - Animation API patterns (use the animation skill)
    
    **Key patterns covered:**
    
    - JS thread vs UI thread mental model and frame budget
    - Profiling with React Native DevTools, platform profilers, and Hermes profiles
    - Re-render optimization (React.memo, useCallback, useMemo, React Compiler)
    - List optimization (FlashList cell recycling, FlatList tuning, memoized items)
    - Image optimization (sizing, caching, preloading, placeholder strategies)
    - Bundle size reduction (tree shaking, named imports, code splitting)
    - Startup time optimization (Hermes bytecode, lazy loading, deferred work)
    - Memory leak detection and prevention
    - Production performance monitoring
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Re-render optimization, list performance, deferred work
    - [examples/profiling.md](examples/profiling.md) - Profiling tools, memory analysis, production monitoring
    - [reference.md](reference.md) - Decision frameworks, performance checklists, targets
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    React Native performance optimization follows one principle: **measure first, optimize second**. Most performance issues stem from a small number of root causes -- unnecessary re-renders, JS thread congestion during animations, unoptimized lists, and memory leaks. Profiling identifies which of these is the actual problem.
    
    **The threading model is key:**
    
    - **JS Thread** -- Runs your React code, business logic, API calls, event handlers. When overloaded, UI updates are delayed and animations stutter.
    - **UI Thread (Main Thread)** -- Renders native views, handles touch events, runs native animations. Must stay free for smooth 60 FPS.
    - **Background Threads** -- Hermes GC, image decoding, network. These don't directly block the UI.
    
    **Frame budget: 16.67ms.** Every frame must complete within this budget on both threads. A single dropped frame is perceptible; consistent drops create jank.
    
    **The optimization hierarchy:**
    
    1. **Architecture** -- New Architecture (Fabric + JSI) provides foundational performance gains. Enable it first.
    2. **Algorithmic** -- Reduce work: fewer re-renders, smaller lists, deferred computation.
    3. **Memoization** -- React Compiler handles most cases automatically. Add manual memoization only where profiling shows it helps.
    4. **Native offloading** -- Move animations to the UI thread (useNativeDriver, animation library worklets), defer heavy work with InteractionManager.
    
    **React Compiler changes the game:**
    
    React Compiler (v1.0, stable since October 2025) auto-memoizes components, hooks, and values at build time. With React Compiler enabled, manual `useMemo`, `useCallback`, and `React.memo` are largely unnecessary. Check your project setup before adding manual memoization -- it may already be handled.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: JS Thread vs UI Thread Optimization
    
    The most common performance issue is JS thread congestion during animations or transitions. When the JS thread is busy, native animations keep running (they're on the UI thread), but React updates stall.
    
    ```typescript
    import { InteractionManager } from "react-native";
    
    // Defer heavy work until after navigation transition completes
    function ScreenWithDeferredLoad() {
      const [data, setData] = useState<Item[]>([]);
      const [isReady, setIsReady] = useState(false);
    
      useEffect(() => {
        const task = InteractionManager.runAfterInteractions(() => {
          const result = expensiveComputation();
          setData(result);
          setIsReady(true);
        });
        return () => task.cancel();
      }, []);
    
      if (!isReady) return <LoadingPlaceholder />;
      return <ItemList data={data} />;
    }
    ```
    
    **Why good:** InteractionManager waits until animations/transitions finish before running heavy work, keeping transitions smooth at 60 FPS
    
    ```typescript
    // BAD: Heavy computation runs immediately, blocking transition
    function BadScreen() {
      const data = expensiveComputation(); // Blocks JS thread during navigation
      return <ItemList data={data} />;
    }
    ```
    
    **Why bad:** Synchronous heavy work during mount blocks the JS thread, causing the navigation animation to stutter or freeze
    
    See [examples/core.md](examples/core.md) for requestAnimationFrame patterns and touch response optimization.
    
    ---
    
    ### Pattern 2: Re-Render Prevention
    
    Unnecessary re-renders are the most common React Native performance problem. Profile first to find which components re-render unnecessarily, then apply targeted fixes.
    
    ```typescript
    import { memo, useCallback } from "react";
    
    // Memoized list item -- only re-renders when props change
    const ProductItem = memo(function ProductItem({
      item,
      onPress,
    }: ProductItemProps) {
      const handlePress = useCallback(() => {
        onPress(item.id);
      }, [item.id, onPress]);
    
      return (
        <Pressable onPress={handlePress}>
          <Text>{item.name}</Text>
        </Pressable>
      );
    });
    ```
    
    **Why good:** memo prevents re-renders when parent re-renders but item props haven't changed, useCallback gives a stable function reference
    
    ```typescript
    // BAD: Inline function creates new reference every render
    <FlatList
      renderItem={({ item }) => (
        <Pressable onPress={() => handlePress(item.id)}>
          <Text>{item.name}</Text>
        </Pressable>
      )}
    />
    ```
    
    **Why bad:** New function reference on every render defeats FlatList's recycling optimization, every item re-renders on any parent state change
    
    **React Compiler note:** If React Compiler is enabled (check your Babel config for `babel-plugin-react-compiler`), it auto-memoizes components and callbacks. Verify with the "Memo" badge in React DevTools before adding manual `memo`/`useCallback`.
    
    See [examples/core.md](examples/core.md) for full re-render optimization patterns with custom comparators.
    
    ---
    
    ### Pattern 3: List Performance (FlashList and FlatList)
    
    Lists are the primary performance concern in mobile apps. FlashList uses cell recycling (reuses component instances) while FlatList uses virtualization (creates/destroys). Key rules: memoize renderItem, never add `key` props to FlashList items, use `getItemType` for heterogeneous lists.
    
    ```typescript
    const ITEM_HEIGHT = 80;
    
    // Stable renderItem with useCallback
    const renderItem = useCallback(
      ({ item }: { item: Product }) => (
        <ProductItem item={item} onPress={onProductPress} />
      ),
      [onProductPress],
    );
    
    <FlashList
      data={products}
      renderItem={renderItem}
      estimatedItemSize={ITEM_HEIGHT}
      getItemType={(item) => item.category}
    />
    ```
    
    **Why good:** useCallback gives stable renderItem reference, getItemType optimizes recycling pools, estimatedItemSize helps initial render (optional in FlashList v2)
    
    See [examples/core.md](examples/core.md) for FlatList tuning props (windowSize, maxToRenderPerBatch), SectionList optimization, and anti-patterns.
    
    ---
    
    ### Pattern 4: Image Optimization
    
    Images are a common source of jank and memory pressure. Key principles: size images appropriately (don't load 4K for thumbnails), use caching, preload critical images, and use placeholders.
    
    ```typescript
    // Key principles for image performance
    const THUMBNAIL_SIZE = 80;
    
    // Size images to their display size, not source size
    <Image
      source={{ uri: thumbnailUrl }}
      style={{ width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE }}
      resizeMode="cover"
    />
    
    // Preload critical images before they're needed
    Image.prefetch(heroImageUrl);
    
    // For image-heavy apps, use an optimized image library
    // that provides: disk/memory caching, blur placeholders,
    // priority loading, progressive rendering
    ```
    
    **Key decisions:** Use the built-in `Image` for simple cases. For image-heavy apps (feeds, galleries, e-commerce), adopt an optimized image library that provides caching, placeholders, and priority loading.
    
    See [examples/core.md](examples/core.md) for image sizing strategies and placeholder patterns.
    
    ---
    
    ### Pattern 5: Bundle Size Reduction
    
    Smaller bundles mean faster downloads and faster Hermes bytecode compilation. Key strategies: use named imports, audit dependencies, enable tree shaking.
    
    ```typescript
    // GOOD: Named import -- tree-shakeable
    import { format } from "date-fns";
    
    // BAD: Namespace import pulls in entire library
    import * as dateFns from "date-fns";
    
    // GOOD: Platform-specific imports reduce per-platform bundle
    // component.ios.tsx -- iOS-only code
    // component.android.tsx -- Android-only code
    ```
    
    **Key strategies:**
    
    - Use named imports for tree-shakeable libraries
    - Audit dependencies with `npx react-native-bundle-visualizer`
    - Remove unused dependencies and dev-only code
    - Use platform-specific files (`.ios.tsx`/`.android.tsx`) to avoid shipping platform-irrelevant code
    - Consider `babel-plugin-transform-remove-console` for production
    
    See [examples/profiling.md](examples/profiling.md) for bundle analysis tools and strategies.
    
    ---
    
    ### Pattern 6: Startup Time Optimization
    
    App startup is the first impression. Hermes compiles JS to bytecode at build time (avoiding JIT at runtime). Beyond Hermes: lazy-load non-critical screens, defer initialization, minimize synchronous work in the root component.
    
    ```typescript
    import { lazy, Suspense } from "react";
    
    // Lazy-load heavy screens that aren't needed immediately
    const AnalyticsScreen = lazy(() => import("./screens/analytics"));
    const SettingsScreen = lazy(() => import("./screens/settings"));
    
    // Defer non-critical initialization
    useEffect(() => {
      const task = InteractionManager.runAfterInteractions(() => {
        initializeAnalytics();
        prefetchUserData();
      });
      return () => task.cancel();
    }, []);
    ```
    
    **Why good:** Lazy loading splits the bundle so non-critical screens don't block initial render, InteractionManager defers initialization until the UI is interactive
    
    **Hermes optimization:** Hermes is enabled by default and compiles JS to bytecode at build time. No configuration needed. For further startup gains, minimize synchronous require() calls and avoid heavy top-level module initialization.
    
    ---
    
    ### Pattern 7: Memory Leak Prevention
    
    Memory leaks in React Native cause gradual performance degradation and eventual crashes. The most common sources: uncleared timers, uncanceled subscriptions, and stale closures in async operations.
    
    ```typescript
    // GOOD: Cleanup all subscriptions and timers
    useEffect(() => {
      const subscription = eventEmitter.addListener("update", handleUpdate);
      const timer = setInterval(pollData, POLL_INTERVAL_MS);
    
      return () => {
        subscription.remove();
        clearInterval(timer);
      };
    }, []);
    
    // GOOD: Cancel async operations on unmount
    useEffect(() => {
      let isMounted = true;
    
      async function fetchData() {
        const result = await api.getData();
        if (isMounted) setData(result);
      }
    
      fetchData();
      return () => {
        isMounted = false;
      };
    }, []);
    ```
    
    **Why good:** Cleanup functions prevent subscriptions from accumulating, isMounted flag prevents state updates on unmounted components
    
    ```typescript
    // BAD: Timer never cleared
    useEffect(() => {
      setInterval(pollData, POLL_INTERVAL_MS); // Leaks on unmount
    }, []);
    
    // BAD: Event listener never removed
    useEffect(() => {
      eventEmitter.addListener("update", handleUpdate); // Accumulates listeners
    }, []);
    ```
    
    **Why bad:** Each mount creates a new timer/listener without removing the old one, memory grows unbounded as components mount and unmount
    
    See [examples/profiling.md](examples/profiling.md) for heap snapshot analysis and memory profiling techniques.
    
    ---
    
    ### Pattern 8: React Compiler (Auto-Memoization)
    
    React Compiler (v1.0, October 2025) eliminates most manual memoization. It analyzes your code at build time and automatically inserts the equivalent of `memo`, `useMemo`, and `useCallback` where beneficial. Available in React Native 0.78+ (React 19).
    
    ```typescript
    // With React Compiler enabled, this component is auto-memoized.
    // No need for React.memo wrapper.
    function ProductCard({ product, onPress }: ProductCardProps) {
      // No need for useCallback -- compiler auto-memoizes
      const handlePress = () => onPress(product.id);
    
      // No need for useMemo -- compiler auto-memoizes
      const formattedPrice = formatCurrency(product.price);
    
      return (
        <Pressable onPress={handlePress}>
          <Text>{product.name}</Text>
          <Text>{formattedPrice}</Text>
        </Pressable>
      );
    }
    ```
    
    **Why good:** Cleaner code with identical performance to manually memoized version, compiler optimizes more consistently than humans
    
    **How to verify:** Open React DevTools Components panel. Components optimized by the compiler show a "Memo" badge. If you see it, manual memoization is redundant for that component.
    
    **When manual memoization is still needed:**
    
    - Components/hooks the compiler can't analyze (complex dynamic patterns)
    - Libraries that haven't been compiled (third-party components)
    - Performance-critical paths where you've profiled and confirmed the compiler missed an optimization
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Should I Optimize This?
    
    ```
    Is there a measurable performance problem?
    |-- NO -> Don't optimize. Premature optimization wastes time.
    +-- YES -> Have you profiled to identify the root cause?
        |-- NO -> Profile first (React Native DevTools Profiler, Perf Monitor)
        +-- YES -> What is the bottleneck?
            |-- JS thread congested -> Defer work (InteractionManager), reduce re-renders
            |-- UI thread dropping frames -> Offload to native (useNativeDriver, worklets)
            |-- List scrolling jank -> FlashList, memoize renderItem, getItemType
            |-- Slow startup -> Lazy load screens, defer initialization
            |-- High memory usage -> Check for leaks (heap snapshots)
            +-- Large bundle -> Named imports, tree shaking, bundle visualization
    ```
    
    ### Manual Memoization Decision
    
    ```
    Is React Compiler enabled in your project?
    |-- YES -> Does the component show "Memo" badge in DevTools?
    |   |-- YES -> Manual memoization is redundant. Don't add it.
    |   +-- NO -> Is this a third-party component or complex dynamic pattern?
    |       |-- YES -> Manual memo/useCallback may be needed. Profile first.
    |       +-- NO -> The compiler should handle it. File a bug if it doesn't.
    +-- NO -> Is this component in a list (renderItem)?
        |-- YES -> Always React.memo + useCallback
        +-- NO -> Does profiling show unnecessary re-renders?
            |-- YES -> Add React.memo, useCallback for callback props
            +-- NO -> Don't memoize. It adds complexity without benefit.
    ```
    
    ### Animation Performance Decision
    
    ```
    What type of animation?
    |-- Layout change (appear/disappear) -> LayoutAnimation (Core Animation, bypasses JS)
    |-- Simple transform/opacity -> Animated API with useNativeDriver: true
    |-- Gesture-driven -> Use your animation library's worklet-based API (runs on UI thread)
    +-- Complex multi-step -> Use your animation library for UI thread execution
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Optimizing without profiling first -- you're guessing, not solving. Profile to identify the actual bottleneck.
    - Testing performance in dev mode -- dev mode adds significant overhead (console logging, error checking, hot reload). Always benchmark in release builds.
    - Inline functions in FlatList/FlashList renderItem -- creates new function reference every render, defeats recycling/virtualization.
    - Adding `key` props to FlashList items -- breaks cell recycling, the core performance advantage of FlashList.
    - Running heavy computation synchronously during screen transitions -- blocks the JS thread, causes transition jank.
    - Using `console.log` in production bundles -- causes JS thread bottlenecks. Use `babel-plugin-transform-remove-console`.
    
    **Medium Priority Issues:**
    
    - Adding manual useMemo/useCallback everywhere without profiling -- adds code complexity, may be redundant with React Compiler.
    - Using ScrollView + map() for lists with 50+ items -- no virtualization, all items rendered in memory simultaneously.
    - Inline style objects in frequently re-rendering components -- creates new object reference every render.
    - Not providing getItemLayout for fixed-height FlatList items -- forces measurement on every scroll, missing a significant optimization.
    - Namespace imports (`import * as`) for large libraries -- prevents tree shaking, inflates bundle.
    
    **Gotchas & Edge Cases:**
    
    - `removeClippedSubviews` helps memory on Android but can cause blank areas on iOS -- use `Platform.OS === "android"` guard.
    - `useNativeDriver: true` only supports non-layout properties (transform, opacity) -- width, height, padding animations must run on JS thread.
    - FlatList `onEndReached` fires immediately if initial data fits the screen -- set `onEndReachedThreshold` carefully and guard against duplicate calls.
    - Hermes heap snapshots show retained objects including JS engine internals -- filter for your app's classes/closures when analyzing.
    - React Compiler cannot optimize components that use `arguments`, `eval`, or non-standard patterns -- these fall back to uncompiled behavior.
    - LayoutAnimation affects ALL layout changes in the next cycle, not just the one you intended -- scope it carefully or use the Animated API for targeted animations.
    - `InteractionManager.runAfterInteractions` tasks are canceled if the component unmounts -- always clean up with `task.cancel()` in useEffect return.
    - FlashList v2 requires New Architecture -- use FlashList v1 or FlatList if on legacy architecture.
    - Dev mode "Perf Monitor" shows in-app FPS but includes dev overhead -- only trust release build measurements.
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST profile BEFORE optimizing -- use React Native DevTools Profiler or platform tools to identify actual bottlenecks, never optimize blindly)**
    
    **(You MUST test performance in RELEASE builds -- dev mode adds significant overhead that masks real performance characteristics)**
    
    **(You MUST understand the JS thread vs UI thread distinction -- animations belong on the UI thread, heavy computation must be deferred with InteractionManager)**
    
    **(You MUST memoize renderItem callbacks and item components for FlashList/FlatList -- inline functions break virtualization performance)**
    
    **(You MUST check if React Compiler is enabled before adding manual useMemo/useCallback/React.memo -- the compiler auto-memoizes and manual hints become redundant)**
    
    **Failure to follow these rules will result in blind optimization that misses real bottlenecks, jank during transitions, and unnecessary code complexity.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related