Skip to content

Instantly share code, notes, and snippets.

@zjhiphop
Forked from mrousavy/MEMOIZE.md
Created May 12, 2021 09:44
Show Gist options
  • Save zjhiphop/0c7280d286ad30cec6138bab606bbec7 to your computer and use it in GitHub Desktop.
Save zjhiphop/0c7280d286ad30cec6138bab606bbec7 to your computer and use it in GitHub Desktop.
Memoize!!! 💾 - a react (native) performance guide
In computing, memoization or memoisation
is an optimization technique used primarily
to speed up computer programs by storing
the results of expensive function calls and  
returning the cached result when the same
inputs occur again.                                         
                                                     — wikipedia

Memoization in React

It's important to memoize heavy computations as well as arrays and object creations so that they don't get re-created on every render. A re-render occurs when state changes, redux dispatches some action, or when the user types into a text input (re-render for every single key press). You don't want to run a lot of operations in those renders for very obvious reasons - so no heavy filtering, no list operations, etc.

Pure Components

A Pure Component (or a React.memo component) does not re-render if it's props are shallow equal.

Each variable you create in your render function will get re-allocated on each render. While this is not a problem for value types, this causes reference types to be different on every render. When you pass those variables down to pure components via props, they will still re-render even though the props are logically the same. Often those variables even go over the Bridge and make your app slow.

Reference equality

When a pure component re-renders, it compares the previous props to the current props and checks if they are shallow-equal.

Value types

Numbers, strings and booleans are value types, which means they can be compared by value:

const i1 = 7;
const i2 = 7;
const equal = i1 === i2; // true

Reference types

Objects, arrays and functions are reference types, which means they cannot be compared by their logical value, but have to be compared by reference:

const o1 = { x: 7 };
const o2 = { x: 7 };
const equal = o1 === o2; // false

Reference comparisons simply compare the memory address of the variable, so only o1 === o1 would be true in the above code example.

There are libraries like deep-equal to compare objects by actual equality, but that's not shallow equality anymore.

React

If you create objects in your render function, they will be re-created on every single render. This means when you create an object in the first render, it is not reference-equal to the object in the second render. For this very reason, memoization exists.

  • Use the useMemo hook to memoize arrays and objects which will keep their reference equality (and won't get re-created on each render) as long as the dependencies (second argument) stay the same. Also use useMemo to cache heavy computations, such as array operations, filtering, etc.
  • Use the useCallback hook to memoize a function.

In general, function components can be optimized more easily due to the concept of hooks. You can however apply similar techniques for class components, just be aware that this will result in a lot more code.

React Native

While animations and performance intensive tasks are scheduled on native threads, your entire business logic runs on a single JavaScript thread, so make sure you're doing as little work as possible there. Doing too much work on the JavaScript thread can be compared to a high ping in a video game - you can still look around smoothly, but you can't really play the game because every interaction takes too long.

Native components (<View>, <Text>, <Image>, <Blurhash>, ...) have to pass props to native via the bridge. They can be memoized, so React compares the props for shallow-equality and only passes them over the bridge if they are different than the props from the last render. If you don't memoize correctly, you might up passing props over the bridge for every single render, causing the bridge to be very occupied. See the Styles example - styles will get sent over the bridge on every re-render!

Here are a few examples to help you avoid doing too much work on your JavaScript thread:

Examples

Styles

Bad

return <View style={[styles.container, { backgroundColor: 'red' }]} />

Good

const style = useStyle(() => [styles.container, { backgroundColor: 'red' }], []);
return <View style={style} />

Exceptions

  • Reanimated styles from useAnimatedStyle, as those have to be dynamic.

See react-native-style-utilities for the useStyle hook







Arrays

Using filter, map or other array operations in renderers will run the entire operation again for every render.

Bad

return <Text>{users.filter((u) => u.status === "online").length} users online</Text>

Good

const onlineCount = useMemo(() => users.filter((u) => u.status === "online").length, [users]);
return <Text>{onlineCount} users online</Text>

You can also apply this to render multiple React views with .map. Those can be memoized with useMemo too.







Functions

Bad

return <View onLayout={(layout) => console.log(layout)} />

Good

const onLayout = useCallback((layout) => {
  console.log(layout);
}, []);
return <View onLayout={onLayout} />

Make sure to also think about other calls in the renderer, e.g. useSelector, useComponentDidAppear - wrap the callback there too!







Forward-propagating Functions

Bad

function MyComponent(props) {
  return <PressableOpacity onPress={() => props.logoutUser()} />
}

Good

function MyComponent(props) {
  return <PressableOpacity onPress={props.logoutUser} />
}







Objects

Bad

function MyComponent(props) {
  return <RecyclerListView scrollViewProps={{ horizontal: props.isHorizontal }} />;
}

Good

function MyComponent(props) {
  const scrollViewProps = useMemo(() => ({
    horizontal: props.isHorizontal
  }), [props.isHorizontal]);
  return <RecyclerListView scrollViewProps={scrollViewProps} />;
}







Lift out of render

Bad

function MyComponent() {
  return <RecyclerListView scrollViewProps={{ horizontal: true }} />;
}

Good

const SCROLL_VIEW_PROPS = { horizontal: true }

function MyComponent() {
  return <RecyclerListView scrollViewProps={SCROLL_VIEW_PROPS} />;
}

This applies to objects as well as functions which don't depend on the component's state or props. Always use this if you can, since it's even more efficient than useMemo and useCallback.







Initial States

Bad

const [me, setMe] = useState(users.find((u) => u.id === myUserId));

Good

const [me, setMe] = useState(() => users.find((u) => u.id === myUserId));

The useState hook accepts an initializer function. While the first example ("Bad") runs the .find on every render, the second example only runs the passed function once to initialize the state.







Count re-renders

When writing new components I always put a log statement in my render function to passively watch how often my component re-renders while I'm working on it. In general, components should re-render as little as possible, and if I see a lot of logs appearing in my console I know I did something wrong. It's a good practice to put this function in your component once you start working on it, and remove it once done.

function ComponentImWorkingOn() {
  // code
  console.log('re-rendering ComponentImWorkingOn!');
  return <View />;
}

You can also use the why-did-you-render library to find out why a component has re-rendered (prop changes, state changes, ...) and possibly catch mistakes early on.







React.memo

Bad

export const MyComponent = (props) => {
  return ...
}

Good

const MyComponentImpl = (props) => {
  return ...
}

export const MyComponent = React.memo(MyComponentImpl);

If your component renders the same result given the same props, you can wrap it in a call to React.memo(...) for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result. See the official docs for React.memo, and use React.memo(...) wisely.







react-native-performance

If your app feels slow, try the react-native-performance library and it's flipper plugin to profile your app's performance in various aspects such as time to interactive, component render time, script execution and more.







Conclusion







memoize!!







Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment