Skip to content

Instantly share code, notes, and snippets.

@mrousavy
Last active April 22, 2024 19:26
Show Gist options
  • Save mrousavy/0de7486814c655de8a110df5cef74ddc to your computer and use it in GitHub Desktop.
Save mrousavy/0de7486814c655de8a110df5cef74ddc 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.







Disclaimer

Don't prematurely optimize. Some examples used here (e.g. the useStyle one) are very small and only demonstrate the idea. A hook like useStyle or useMemo also comes with a cost (allocating the function and the deps array, calling the actual hook and running an array comparison), so keep in mind that it is often better to just pass in objects or arrays directly if the component itself is optimized. After a certain component complexity or with a certain dependency graph, memoizing functions can be a huge performance win, but there are also cases where it just leads to unnecessarily complex code and sometimes even worse performance. Always benchmark before and after!







Conclusion







memoize!!







@henrymoulton
Copy link

henrymoulton commented Mar 27, 2021

Awesome collection of tips!

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

Is there a reason for this?

@mrousavy
Copy link
Author

Is there a reason for this?

Yes. useMemo and useCallback always create a new function, and internally compares dependencies for each render to see if the function has to be rebuilt. If you lift the entire thing out of your render function, you don't need to do those things, as the thing is a constant and only gets created once. No need to use useMemo or useCallback when dependencies are empty ([]) and you don't need access to anything inside the component (including refs, setState, etc)

@griimick
Copy link

griimick commented Aug 6, 2021

Do we need to memoize styles returned by StyleSheets? I cannot find good information on that except for some notes in the docs.

NOTE: Exercise caution as abusing this can tax you in terms of optimizations. IDs enable optimizations through the bridge and memory in general. Referring to style objects directly will deprive you of these optimizations.

source: https://reactnative.dev/docs/stylesheet#flatten

return <View style={styles.container} />
return <View style={[styles.container, styles.custom]} />
return <View style={(condition) ? styles.active : styles.inactive} />

@mrousavy
Copy link
Author

mrousavy commented Aug 6, 2021

@griimick

IDs enable optimizations through the bridge and memory in general

this is BS. The React Native docs are wrong about this, no optimizations (ID referencing) are being performed here. The whole style is just passed over the bridge.

Also you are not memoizing the entire style container and custom, you are just memoizing the array that contains two references to those style objects.

So in theory, this:

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

is faster than this:

return <View style={[styles.container, styles.custom]} />

and the better and even faster variant would be:

const style = [styles.container, styles.custom] // not created in render func

function MyComponent() {
  return <View style={style} />
}

but keep in mind that these are very small optimizations, allocating an array is really fast and you should consider clean code over performance if the overhead is not too big.

@griimick
Copy link

griimick commented Aug 6, 2021

Thank you for the detailed answer. I understand these are very small optimizations but good to know for the worst times.

@fobos531
Copy link

fobos531 commented Sep 18, 2022

Hey @mrousavy , great writeup!

Based on reading this, I got the impression that we should strive to memoize stuff as much as possible. If we are to do that, I wanted to get your opinion on the following:

Are there any memory concerns that we would need to be aware of if we're excessively memoizing things? Like, would memoizing all of that make any problems in terms of e.g. having the app use too much RAM or anything like that? I'm primarily asking this because I've read other articles on the web where the counter-argument is made to using useMemo and useCallback which make the point that in some cases the performance benefits are not worth it and that in that case "more memory" is being allocated than if we were not to memoize it.

Example references:

Thanks in advance!

@mrousavy
Copy link
Author

@fobos531 yes, memoizing stuff means it uses more memory. Another downside of memoizing is that it has some added runtime cost, for useMemo this would allocate a function (first arg) and an array (second arg), then run the hook (func. stack alloc) and compare the dependency array (loop).

The idea of this doc is to understand how memoization works on a simple scale, get a feel for it and memoize stuff where it makes sense - some components are so light that memoizing stuff is not worth it. Always make sure to benchmark before and after, in most cases you're fine with your gut feeling.

My general rule:

  • Always memoize callbacks (every func in a component should be a useCallback) - this also looks simpler in code
  • Sometimes memoize results from functions (e.g. if you do a call to a cryptographic function, make sure that's memoized as that can be slow)
  • Rarely optimize arrays like style={[...]} (just not worth it)

@mrousavy
Copy link
Author

Good example:

function decrypt(string) {
  ... // really heavy crypto function, takes ~200ms
}

function ChatMessage({ encryptedMessage }) {
  // memoize the result
  const message = useMemo(() => decrypt(encryptedMessage), [encryptedMessage])

  return <View>...</View>
}

If this wasn't using useMemo, the decrypt function would get called everytime ChatMessage() re-renders - which can happen quite a lot.

@putuoka
Copy link

putuoka commented Feb 3, 2023

Good example:

function decrypt(string) {
  ... // really heavy crypto function, takes ~200ms
}

function ChatMessage({ encryptedMessage }) {
  // memoize the result
  const message = useMemo(() => decrypt(encryptedMessage), [encryptedMessage])

  return <View>...</View>
}

If this wasn't using useMemo, the decrypt function would get called everytime ChatMessage() re-renders - which can happen quite a lot.

But why do u use usememo in function? Didn't you say use usecallback for functions?

@pavlobu
Copy link

pavlobu commented Feb 23, 2023

useMemo memoizes not a function, but what it actually returns. so the result of decrypt(...) call is memoized. useCallback memoises a function definition that later can be called in the code

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