as

Settings
Sign out
Notifications
Alexa
Amazon Appstore
AWS
Documentation
Support
Contact Us
My Cases
Get Started
Design and Develop
Publish
Reference
Support

App Performance Best Practices

Since React Native for Vega apps are just React Native apps, public documentation on React Native best practices also apply. Consider these key best practices when building React Native for Vega apps, along with key differences for Vega compared to iOS or Android.

Avoiding re-renders and re-definitions

memo

Using React's memo improves performance by preventing unnecessary re-renders of functional components. By default, React re-renders a component whenever its parent re-renders, even if its props haven't changed. React's memo optimizes this by shallowly comparing the previous and new props, skipping a re-render if they are the same. This is beneficial for components that receive stable props that don't need to update frequently, such as list items, buttons, or UI elements with heavy rendering logic. When combined with useCallback and useMemo, memo can also minimize wasted renders, improving efficiency in React apps.

By default, memo performs a shallow comparison of props using the Object.is static method to determine if a component should re-render. In some cases, you may need to provide your own arePropsEqual function to customize this comparison. A common use case for this is when passing inline styles or anonymously defined prop values to your function component. In these cases, the props are equal by value but not in memory, so the Object.is() comparison will mark them as not equal. A customized comparison is useful when dealing with complex objects or functions as props, where a shallow comparison isn’t sufficient. A custom comparison function allows finer control over when a component should update, improving performance in scenarios where you require deep comparisons or specific conditions.

Two useful tools to investigate React component re-renders the "Record why each component rendered while profiling" flag from the react-devtools profiler, and the community package why-did-you-render (WDYR).

It's generally best to wrap the user defined function components in the memo API. However, using memo and useMemo everywhere may not be useful. See React’s article, Should you add memo everywhere? on when and how to use memo and useMemo.

React compiler and eslint-plugin-react-compiler

In React 19 release candidate (RC), React has introduced a static React code compiler that automatically applies memoization through memo, useMemo, and useCallback. The compiler assumes your app follows the Rules of React, is semantic JavaScript or TypeScript, and tests for nullable and optional values and properties before accessing them. While React says "the compiler is experimental and has many rough edges", we recommend that you onboard to the react compiler's ESLint plugin, eslint-plugin-react-compiler, which does not require upgrading to React 19. To onboard, see Installing eslint-plugin-react-hooks.

Strict mode

Wrapping a React Native app in <StrictMode> is beneficial because it helps identify potential issues in the app during development. <StrictMode> runs additional checks and warns about deprecated lifecycle methods, unsafe side effects, and other potential problems that could lead to performance issues or bugs. It also double-invokes certain functions, like component constructors and useEffect callbacks, so no unintended side effects result. This debugging tool helps developers catch problems early, leading to more stable and efficient apps. <StrictMode> doesn’t impact production builds, but using it in development ensures best practices and prepares the app for future React and React Native updates.

Suspense and lazy

React's lazy component and <Suspense> boundary can significantly improve startup time and overall app performance. By enabling code splitting, React's lazy allows components to load only when needed, reducing the initial bundle size and speeding up the app's initial render. React's <Suspense> works alongside this by allowing the app to "suspend" rendering until the necessary components load, creating a smooth user experience without blocking the UI. This approach not only optimizes app startup times but also enhances bundle loading efficiency, loading only the required code for each view.

useCallback

Using useCallback in React improves performance by memoizing function instances, preventing unnecessary re-creations on each render. useCallback is beneficial when passing functions as props to child components. This helps avoid unnecessary re-renders when those components rely on referential equality checks. For instance, when it’s wrapped in React.memo. useCallback is also useful where functions are effect dependencies (useEffect), preventing unintended re-executions of side effects. Use useCallback judiciously, since excessive use may increase memory usage and complexity. When used correctly, useCallback optimizes rendering efficiency and reduces performance overhead in React apps.

You should add items to the dependency array for useCallback so the memoized function has access to the latest values of any reactive dependencies it references. Omitting dependencies can lead to stale closures, where the function continues to use outdated values from previous renders, which may cause unexpected behavior or bugs.

useEffect()

Although not specific to React Native, how you use React's useEffect can impact performance. It is worth reading React's article, You Might Not Need an Effect. As always, ensure your useEffect dependency array contains all reactive values (props, state, variables, and functions declared within your component body, etc.). Using the eslint-plugin-react-compiler will help you detect missing dependencies in your useEffect's dependency array.

useMemo

React's useMemo improves performance by memoizing the result of expensive computations, preventing unnecessary recalculations on every render. Without useMemo, functions that perform complex operations—such as filtering large lists, performing mathematical calculations, or processing data—would execute on every render, even if their dependencies haven't changed. By wrapping such computations in useMemo, React only re-evaluates them when their dependencies update, reducing CPU usage and improving responsiveness. This is particularly useful in scenarios where a component frequently re-renders due to unrelated state changes. Use useMemo selectively, since excessive use can add memory overhead without significant benefits.

You should add items to the dependency array for useMemo to ensure that the memoized value is recalculated whenever any of its dependencies change. Omitting dependencies can result in stale or incorrect values, while unnecessary dependencies can cause unnecessary recalculations, reducing performance benefits.

useState and useTransition

Minimizing React's useState usage can improve performance by reducing unnecessary re-renders. Every state update triggers a re-render, so excessive state management can lead to performance bottlenecks, especially in complex components. By minimizing useState usage, such as by using refs (useRef) for mutable values that don't affect rendering, or by deriving state instead of storing redundant values, you can make components more efficient. The useTransition hook enhances performance by prioritizing urgent state updates, such as user input. At the same time, it defers less critical state updates such as search results. This prevents UI blocking, ensuring a smoother experience by keeping interactions responsive even during expensive state transitions. Wrapping set state calls in the startTransition function from useTransition allows React to interrupt renders for stale state updates, and prioritizes the render for the most recent state update.

Sample app

Before optimization

Copied to clipboard.


import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  Button,
  FlatList,
  TouchableOpacity
} from 'react-native';

// Bad practice: Non-memoized data and function
const BadPracticeApp = () => {
  const [count, setCount] = useState(0);
  const [selectedItem, setSelectedItem] = useState(null);

  // Expensive calculation on each render, and new function instance on BadPracticeApp level re-render
  const sumOfSquares = (num) => {
    console.log('Calculating sum of squares...');
    return Array.from(
      { length: num },
      (_, i) => (i + 1) ** 2
    ).reduce((acc, val) => acc + val, 0);
  };

  // data is recreated on every BadPracticeApp re-render
  const data = Array.from(
    { length: 50 },
    (_, i) => `Item ${i + 1}`
  );

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Sum of squares: {sumOfSquares(10)}</Text>
      <Button
        title="Increase Count"
        onPress={() => setCount(count + 1)}
      />
      <FlatList
        data={data}
        keyExtractor={(item) => item} // anoynmous function used, so new function instance created on every re-render
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => setSelectedItem(item)}>
            <Text
              style={{
                padding: 10,
                backgroundColor:
                  item === selectedItem ? 'lightblue' : 'white'
              }}>
              {item}
            </Text>
          </TouchableOpacity>
        )} // anoynmous function used, so new function instance created on every re-render and returned function component is not memoized
      />
    </View>
  );
};

export default BadPracticeApp;

After optimization

Copied to clipboard.


import React, {
  memo,
  StrictMode,
  useState,
  useMemo,
  useCallback,
  useTransition
} from 'react';
import {
  View,
  Text,
  Button,
  FlatList,
  TouchableOpacity,
  ActivityIndicator
} from 'react-native';

// Memoized Item component to prevent unnecessary re-renders
const Item = memo(({ item, isSelected, onPress }) => {
  return (
    <TouchableOpacity onPress={() => onPress(item)}>
      <Text
        style={{
          padding: 10,
          backgroundColor: isSelected ? 'lightblue' : 'white'
        }}>
        {item}
      </Text>
    </TouchableOpacity>
  );
});

const BestPracticeApp = () => {
  const [count, setCount] = useState(0);
  const [selectedItem, setSelectedItem] = useState(null);
  const [isPending, startTransition] = useTransition(); // For handling non-urgent updates

  // Memoize the expensive calculation
  const sumOfSquares = useMemo(() => {
    console.log('Calculating sum of squares...');
    return Array.from(
      { length: 10 },
      (_, i) => (i + 1) ** 2
    ).reduce((acc, val) => acc + val, 0);
  }, []); // Only recalculated if dependencies change (none in this case)

  // Memoize the data to avoid unnecessary array recreation
  const data = useMemo(
    () => Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`),
    []
  );

  // Memoize the renderItem function to avoid unnecessary re-creations
  const renderItem = useCallback(
    ({ item }) => (
      <Item
        item={item}
        isSelected={item === selectedItem}
        onPress={handleSelectItem}
      />
    ),
    [selectedItem] // Only re-created when selectedItem changes
  );

  // Simulate a delayed process (e.g., fetching additional data) when an item is selected
  const handleSelectItem = (item) => {
    startTransition(() => {
      setSelectedItem(item); // Mark the selected item
    });
  };

  return (
    <StrictMode>
      <View style={{ flex: 1, padding: 20 }}>
        <Text>Sum of squares: {sumOfSquares}</Text>
        <Button
          title="Increase Count"
          onPress={() => setCount(count + 1)}
        />
        <Text>Count: {count}</Text>

        <FlatList
          data={data}
          keyExtractor={(item) => item}
          renderItem={renderItem}
          getItemLayout={(data, index) => ({
            length: 50,
            offset: 50 * index,
            index
          })}
        />

        {isPending && (
          <ActivityIndicator size="large" color="#0000ff" />
        )}
      </View>
    </StrictMode>
  );
};

export default BestPracticeApp;

How to handle large lists

React Native offers quite a few different list components.

ScrollView

Within the context of performance, it is typically better to use the FlatList component over the ScrollView component since FlatList virtualization renders child components lazily. The ScrollView component renders all its React child components at once. For that reason, a large number of child components for your ScrollView component, the more performance is impacted. Rendering will slow down and memory usage will increase. However, for lists with a smaller data set, using ScrollView is perfectly acceptable. Remember to memoize the ScrollView and its children components accordingly.

FlatList

Setting the FlatList required data and renderItem props does't unlock all of the performance benefits of using a FlatList component. Set other props in your FlatList component to improve your app's fluidity (FPS), CPU utilization, and memory usage. Apply all of the best practices outlined in React Native's official Optimizing FlatList Configuration article. In our testing, we've found getItemLayout, windowSize, and initialNumToRender props as well as adding memoization to child components especially important for fluidity and CPU utilization. If your app involves nested FlatList usage to support vertical and horizontal scrolling, you should memoize your nested FlatList components.

FlashList

Vega fully supports Shopify’s FlashList, which provides a more performant alternative to React Native’s FlatList component. Swapping from FlatList to FlashList is trivial, since both have the same component props. However, some FlatList specific props no longer work for FlashList. These props include windowSize, getItemLayout, initialNumToRender, maxToRenderPerBatch, and updateCellsBatchingPeriod. You can find a full list at the end of this usage article. Set a few key props, and follow these best practices, to unlock better performance for FlashList.

  • First, make sure to set the estimatedItemSize prop for your FlashList. Using the estimatedItemSize prop in FlashList helps improve performance by allowing the list to pre-render the appropriate number of items, minimizing blank space and load time, while also enhancing responsiveness during fast scrolls by avoiding unnecessary re-renders and large render trees. For more details, you can read this Estimated Item Size Prop article.
  • Using item recycling in FlashList improves performance by reusing off-screen components instead of destroying them, preventing unnecessary re-renders and reducing memory usage. To optimize this, avoid using useState for dynamic properties in recycled components, since state values from previous items can carry over, leading to inefficiencies. For more details, see this Recycling article.
  • Remove the key prop from item components and their nested components. For more details, see this Remove key prop article.
  • If you have different types of cell components and they differ quite a lot, consider leveraging the getItemType prop. For more details, see this getItemType article.

The Image component

React Native's Image component on iOS and Android does not provide out-of-box performance optimizations, such as caching. React Native developers usually use a community package such as react-native-fast-image or expo-image for memory or disk level caching for images in their React Native app. For React Native for Vega, we've built caching mechanisms throughout the native implementation of the Image component. They perform like an Image from react-native-fast-image or expo-image.

We recommend you have the same image asset available in several sizes and resolutions for your use case. For example, if rendering Image components within a FlatList or ScrollView component, it's better to use a cropped version or a thumbnail sized asset. This will lead to fewer CPU cycles spent decoding your image and less memory usage for the raw image asset.

The Animated library

React Native provides an Animated library for fluid animations on React Native's core components. Animations are expensive operations in React Native. Frame by frame, the JS thread computes updates to the animation to send over the bridge to the native side to generate the frame. Using animation can also affect other processes running at the same time. This can overload the JS thread so it cannot process timers, executing React hooks, component updates, or other processes. To avoid this, set the useNativeDriver prop on your animation to true. useNativeDriver is set to true in native animations, while useNativeDriver is set to false` in JS animations. For more on this, see Using the native driver.

It's also recommended that you schedule the animation with InteractionManager. InteractionManager.runAfterInteractions() takes a callback function, or a PromiseTask object, which executes only after the current animations or other interactions are completed. Delaying the execution of a newly scheduled animation reduces the chance of overloading the JS thread and helps improve overall app responsiveness and fluidity.

Optimizing your focus management UI

When a component gains focus, its onFocus callback is invoked, and when a component loses focus, its onBlur callback is invoked. This is the primary way to drive UI changes while the user navigates through your app. Keep your onFocus and onBlur handler functions as simple as possible. There are several ways to do this.

If your component is only drawing a border around the focused item, use conditional styling. For example, styles={isFocused ? styles.focusedStyle : styles.blurredStyle}. Make sure every combination of your onFocus and onBlur invocation causes at most one React render cycle. This way the JS thread performs minimal work and the user navigation remains responsive.

If your component requires more complex UI updates when the focus is gained for a component, perform that work through a native animation so the work is not done on the JS Thread itself. Examples of such a UI updates are enlarging the selected view, text, or image, or changing the opacity. To optimize further, add a debounce mechanism where this native animation starts only if the user focuses on the item for over a set amount of time. While you will incur a slight penalty of the animation starting a bit later, this delay will prevent the animation from kicking off for every focused item when the user is fast scrolling or navigating in your app.

Listeners, event subscriptions, and timers

Some Turbo Modules (TMs) such as VegaAppState, DeviceInfo, or Keyboard allow you to register listeners on certain events. These listeners are often created within a useEffect hook. To avoid any dangling memory, clean up your listeners in the return function of your useEffect hook.

Copied to clipboard.

useEffect(() => {
  const keboardShowListenerHandler = Keyboard.addListener(
    'keyboardDidShow',
    handleKeyboardDidShow
  );
  return () => {
    keboardShowListenerHandler.remove();
  };
}, []);

Reducing overdraw

Your React Native for Vega app is like a canvas. When you have nested views with different background colors occupying the same space, you're painting over the same area over and over. For example, if a full-screen black view is covered by a full-screen grey view, the black layer becomes "fully overdrawn." That makes it invisible, but it is still processed. While some overdraw is acceptable, especially it it's partial, minimizing these redundant draw operations improves performance. The goal is to draw each pixel as few times as possible per frame. You can detect overdraw in your app by running your app with the ?SHOW_OVERDRAWN=true launch query argument.

Copied to clipboard.

vda shell vlcm launch-app "pkg://com.amazon.keplersampleapp.main?SHOW_OVERDRAWN=true"

UI elements on your canvas will now contain a semi-transparent tint to them. The resulting color indicates how many times a canvas was overdrawn.

  • True color: No overdraw
  • Blue color: Overdrawn 1 time.
  • Green color: Overdrawn 2 times.
  • Pink color: Overdrawn 3 times.
  • Red color: Overdrawn 4 or more times.

Ideally, no part of your app should contain a pink or red tint, meaning don’t overdraw it with 3 or more layers.

Reducing bundle size

With any React app, it's crucial to minimize your bundle size for optimizing memory usage and improving app launch time. react-native-bundle-visualizer can help identify expensive import statements or unused dependencies. Once it’s installed, you can run the following command to automatically open an HTML file in your browser for you to interactively view.

Copied to clipboard.

npx react-native-bundle-visualizer

Here’s a sample React Native for Vega app bundle that is trying to use debounce from lodash via import { debounce } from 'lodash';. After this import statement, you can visualize the bundle like this.

Large bundle size (click on image enlarge)

From the image above, we can see that lodash is 493.96 KB (17.8%). After switching this import statement to import debounce from 'lodash/debounce';, the bundle size reduces dramatically.

Small bundle size (click on image enlarge)

Now, lodash is only 13.19 KB (0.6%). Changing this import statement reduced the app's bundle size by 480.77 KB.

Improving initial app launch times

Utilize Vega's Native SplashScreen API to display your splash screen efficiently. This API renders a splash screen natively from raw image assets bundled in your vpkg, ensuring immediate visibility when your app launches. This approach frees up your app's JavaScript thread to handle critical tasks like content loading and network calls, instead of rendering a JavaScript based splash screen. The native splash screen automatically dismisses after your app's first frame renders. Two methods are used to customize when a splash screen is dismissed. Call usePreventHideSplashScreen(), which overrides the splash screen auto-dismissal, and also call useHideSplashScreenCallback(), which hides the splash screen.


Last updated: Sep 30, 2025