React Native developer. Code for tattoos !
React is great and fast most of the time. But sometimes, due to heavy calculations, it slows down, that’s when we need to measure and optimize our Components to avoid “wasted renders”.
Optimizations come with its cost, if it’s not done properly, the situation might get worse. In today’s blog post, we get to know the rendering process, learn the cause of wasted renders, solutions & how it’s broken.
Table of contents
- What is “Rendering” ?: Process Overview, Render & Commit Phase
- Standard Render Behavior: The cause of wasted renders in Render Phase
- Improving Rendering Performance: Some techniques
- How New Props References break Optimizations: The detail of the problem
- Optimizing Props References: useMemo & useCallback
- Memoize Everything?
What is “Rendering” ?
Rendering is the process of React asking your Components to describe what the section UI looks like, on the current combination of Props and State.
During the process, React will start at the root of the component tree and loop downwards to find the components flagged as needing updates. For each flagged components, it will call render()(for class components) or FunctionComponent()(for function components), and save the render outputs.
A component render’s output is written in JSX. Either from render() or FunctionComponent(), the output eventually becomes ReactElement. These elements are used together to form the virtual tree (tempt tree).
After collecting the new tree, React will diff it, collect lists of all changes need to be applied to make the real tree look like the current desired output. This process is called Reconciliation.
Above is very basic process to create Host tree (the tree output). The host tree can be vary of types, base on different platforms (web, mobile,…). Dan Abramov wrote a great explanation for it Here.
React team divides above work into 2 phases:
- The “Render phase” contains all the work of rendering components and calculating changes.
- The “Commit phase” is the process of applying those changes to the host tree.
More layers for React Native (You can skip this and move on if you’re not interested)
React Native creates a tree hierarchy to define the initial layout and creates a diff of that tree on each layout change like above. Except React Native manages the UI updates through couple of architecture layers that in the end translate how views should be rendered.
1. The Yoga layout engine
Yoga is a cross-platform layout engine written in C which implements Flexbox through bindings to the native views (Java Android Views / Objective-C iOS UIKit).
All the layout calculations of the various views, texts and images in React-Native are done through yoga, this is basically the last step before our views get displayed on the screen
2. Shadow tree/Shadow nodes
When react-native sends the commands to render the layout, a group of shadow nodes are assembled to build shadow tree which represented the mutable native side of the layout (i.e: written in the corresponding native respective language, Java for Android and Objective-C for iOS) which is then translated to the actual views on screen (using Yoga).
So React Native basically still uses React’s ability to calculate The difference between the previous and the current rendering representation and dispatches the events to the UIManager accordingly.
Standard Render Behavior
It is important that:
React’s default behavior is that when a parent component renders, React will recursively render all child components inside of it!
For example, say we have a component tree of A > B > C.
- We trigger a re-render in B (setState or setter of useState).
- React starts the render pass from the top of the treeReact sees that A is not marked as needing an update, and past it
- React sees that B is marked as needing an update, and renders it. B returns <C /> as it did last time.
- C was not originally marked as needing an update. However, because its parent B rendered, React now moves downwards and renders C as well.
Now, it’s likely that most of the components will return the exact render result as last time, therefore, React won’t need to make change to the real tree. However, React will still have to ask the components re-render themselves and diff the render output. Both of those take time and effort, especially when the components are large & have heavy calculations.
This is how wasted renders happens.
Improving Rendering Performance
Renders are normal expected part of React. It’s also true that sometimes the effort is wasted if a component’s render output hasn’t changed, and that part of the tree doesn’t need updating.
Render should always based on current Props and State of component. If we know ahead of time that Props and State won’t change. the render output won’t change, then we can safely skip the rendering process of that component.
When it comes to optimization, you can make it run faster or do less work. Most of React optimization is about doing less work.
Remember to measure before any optimization, so you don’t commit premature optimization.
React offers three primary APIs for skipping rendering of a component.
- React.Component.shouldComponentUpdate: A component lifecycle happens early in the render process. (happening in updating lifecycle). If return false, React will skip rendering component. By default, it always return true, so when you need to skip rendering component, you can add your own logic. Commonly, when we custom this lifecycle, we compare the old props, state with the new ones, and return false if nothing change.
- React.PureComponent: Since the comparison of props and state is the most common way to implement shouldComponentUpdate. PureComponent is a base class implements that behavior by default. Can be used instead of React.Component + shouldComponentUpdate.
- React.memo: A built-in higher order component. It accepts your component and return a new wrapper component. Wrapper component’s default behavior is to check if any props has changed, if not, it prevents rendering. It also accept your custom logic for the comparison work, commonly this is used to compare specific props, instead of all of them.
All of these approaches use a comparison technique called Shallow Equality. This means checking individual field in two different objects, and seeing if any difference in the contents of objects. The technique compares with ===, a simple and fast way JS engine can do.
How New Props References break Optimizations
As we learned about techniques with shallow equality above, it’s apparent that passing new objects will fail the comparison because “===” compares reference, even if the contents haven’t changed. That breaks our optimizations, the component still renders, but wasting more diffing effort, diffing through props comparison & diffing tree. Be careful !
In the example, we pass onClick and data as props to MemoizedChildComponent. Although we optimize ChildComponent, it still re-render every ParentComponent updating. Because MemoizedChildComponent’s props get new objects every time.
We expect MemoizedChildComponent skip rendering because it’s props contents are the same. Let’s move on and figure how we can fix this.
Optimizing Props References
Class components don’t have to worry about accidentally creating new callback object references as much, because they can have instance methods that are always the same reference. However, they may need to generate unique callbacks for separate child list items, or capture a value in an anonymous function and pass that to a child. Which resulting in new objects, React hasn’t come with any built-in to optimize those cases.
The intention of this post is to draw the problem out, not teaching about Hook, i believe there are many sources explaining those Hook well. So i’m not going into detail here. Maybe in the next posts, who knows right ^^
Apparently NO, every optimization comes with its cost. Optimizing carelessly ends up making the performance worse, always measure first, by React devtool or any of your favorite, find the bottleneck, and then optimize.
It’s not always benefit, if it was, React would make it the default implementation, right ? 😀
Kent C. Dodds mentioned a case when useCallback worse here. And my favorite Dan’s tweet:
Why doesn’t React put memo() around every component by default? Isn’t it faster? Should we make a benchmark to check? Ask yourself: Why don’t you put Lodash memoize() around every function? Wouldn’t that make all functions faster? Do we need a benchmark for this? Why not?
Well, that’s the end of this post.
To summary, Rendering process of React renders children components due to updating parent components, that’s not bad, that’s how React knows the changes. And sometimes the rendering effort is wasted.
Skipping rendering is a common way to optimize this, and the work relates to props references a lot. Optimizing with care, don’t premature optimization.
Props Reference has more problems than that. Recently, i love Ben’s article on how it affects dependencies in useEffect hook.
For any further questions or comments, let me know. Thanks !
Create your free account to unlock your custom reading experience.