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
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.
Process Overview
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:
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.
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
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).
The ViewManger is an interface that knows how to translate the View Types sent from the JavaScript into their native UI components. The ViewManager knows how to create a shadow node, a native view node and to update the views. In the React-Native framework, there are a lot of ViewManager that enable the usage of the native components. If for example, you’d someday like to create a new custom view and add it to react-native, that view will have to implement the ViewManager interface
The UIManager is the final piece of the puzzle, or actually the first. The JavaScript JSX declarative commands are sent to the native as Imperative commands that tell React-Native how to layout the views, step by step iteratively. So as a first render, the UIManager will dispatch the command to create the necessary views and will continue to send the update diffs as the UI of the app changes over time.
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.
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.
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.
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.
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.
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.
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.
Function component, React offer two hooks useCallback (for callback function) and useMemo(for any kind of data like creating objects or complex calculations).
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 ? :D
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 !
Also published on: https://medium.com/javascript-in-plain-english/react-native-why-props-references-break-optimizations-79c463ca0723