Parents & Owners in React: Rendering Performance
Being aware of the distinction between parent and owner components can help you isolate updates and improve rendering performance.
In React, the distinction between parent and owner components is fundamental, important, and very under-discussed.
- The owner is the component that renders a particular component.
- The parent is the component (or element) in which that particular component is nested.
Once you’re aware of it, you’ll start seeing it everywhere. You’ll notice how overloaded the word ‘parent’ is in React and how people really mean ‘owner’ when they say ‘parent’.
Parents vs Owners Quiz
If this is unclear, read my previous post Parents & Owners in React: Data Flow and do the quiz here.
Look at this codeblock:
function Post() { return ( <Article title="Parent vs Owner"> <Section title="Data Flow"> <p>Content</p> </Section> </Article> );}function Article({ children }) { return ( <article> <h1>{title}</h1> {children} </article> );}function Section({ children, title }) { return ( <section> <h2>{title}</h2> {children} <Callout /> </section> );}function Callout() { return ( <aside> <strong>Subscribe</strong> </aside> );}
Now try to answer these questions:
Which component is the parent of <Section>
?
Which component is the owner of <Section>
?
Which prop is a React element?
Which component is passed down as a React element?
Which component are slotted components?
Data flow (props) follows the owner or parent tree (select one)
And here are the diagrams for both the trees:
If you had trouble answering these questions. I recommend you read the previous post before reading on. It’s important to get this difference right.
Ideally, the UI of our React apps is snappy and the code for it easy to follow. How can being aware of this distinction help with this? In the previous post Parents & Owners in React: Data Flow, we saw how being aware of the parent/owner difference can help make data flow of a React app easier to follow. But what does it have to do with rendering performance and snappy UI’s?
To answer this question, we must first answer another, more fundamental question.
A component is most commonly described as a “piece of the UI”. It’s not a bad way to think about it, but not particularly helpful for rendering performance. It doesn’t reflect the amount of work done to create that part of the UI and keep it up-to-date. Keep in mind that re-renders generally happen more than mount and unmount.
For performance, it helps to think of a React component as a unit of update. A component can schedule an update (a re-render) by changing state. It’s a rather technical definition but a very solid one.
At its core, React is essentially a library for processing a queue of state updates to produce consistent UIs.
If we view a component as a unit-of-update, we can view putting components together as dividing the work for keeping the UI up-to-date. If we’re mindful of how we divide the work to update, we could do less work updating and improve rendering performance.
Why do we even split apps into components? First, it’s useful to turn it around and imagine how we can make performance worse. We could write a React app as a single component. All JSX and state packed in to one. Every change to state would re-render everything. We’ll have just one big unit-of-update, without division of work.
It’s easy to see why we don’t. React shouldn’t need to update UI unrelated to a particular change of data. Components provide a way to split the UI into parts that should be updated. A component only updates itself and the components it renders; the element tree it owns, that’s the unit-of-update.
Which brings me to another important distinction. There are two reasons for a component to re-render 1:
- Self-update: React will call the component whose state update triggered the render.
- Owner-update: By default, when a component re-renders, all of the components it renders (owns) are called too. This process is recursive, updates (re-renders) cascade down the owner tree.
I’ll reuse the example from the previous post. currentUser
state is placed in <App>
.
I’ve added this state to the diagram and made it interactive too. Go ahead and click currentUser
it to what updating it does.
Q: Which component self-updates?
Slotted components have a ‘hole in their middle’. They take React elements as props and return them. This way they let other components place elements inside of them. This makes it possible to change which component “owns” a particular element without affecting the DOM structure and UI.
What makes slotted components really neat is that they enable a built-in rendering optimization.
A passed-down element (children
or JSX-as-named-props) is created by the owner, not the parent.
If the parent re-renders by a self-update, the owner does not, meaning the passed-down element is not recreated.
It is the same element (as in referentially equal) as last render.
React knows there is no point in rendering it and skips it 2 3.
We can use this to untangle components into parts that change and parts that don’t change, leading to smaller units-of-update. Then we can combine these parts back together to keep the same parent structure/UI. This is how slotted components enable splitting the work for updates and provide a natural optimization opportunity 4.
A less natural optimization opportunity is to wrap child elements in useMemo
. This works for the same reason but is much less common.
It’s hard coming up with examples and even harder to come up with a better one than than the one from Before You memo by Dan Abramov, so I’ll just use that one. I recommend you read that post, it’s really good and not that long.
App
owns ExpensiveTree
and contains color
state.
Updating color
will trigger a re-render of App
which in turn will render ExpensiveTree
, which has no need for it.
color
state is placed too high and therefore App
does too much work updating.
function App() { const [color, setColor] = useState("red"); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> <p>Hello, world!</p> <ExpensiveTree /> </div> );}//
We need to untangle state — and by extension, updates — from parts of the UI that don’t need it.
If you can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common parent component.
First, make a new, slotted component to hold the often-updated state.
function ColorPicker({ children }) { const [color, setColor] = useState("red"); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> {children} </div> );}//
Then, seperate the elements irrelevant to that state, <p>
and <ExpensiveTree />
, and pass them down as children
.
function ColorPicker({ children }) { const [color, setColor] = useState("red"); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> {children} </div> );}function App() { return ( <ColorPicker> <p>Hello, world!</p> <ExpensiveTree /> </ColorPicker> );}
App
owns ExpensiveTree
and contains color
state.
Updating color
will trigger a re-render of App
which in turn will render ExpensiveTree
, which has no need for it.
color
state is placed too high and therefore App
does too much work updating.
We need to untangle state — and by extension, updates — from parts of the UI that don’t need it.
If you can’t find a component where it makes sense to own the state, create a new component solely for holding the state and add it somewhere in the hierarchy above the common parent component.
First, make a new, slotted component to hold the often-updated state.
Then, seperate the elements irrelevant to that state, <p>
and <ExpensiveTree />
, and pass them down as children
.
function App() { const [color, setColor] = useState("red"); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> <p>Hello, world!</p> <ExpensiveTree /> </div> );}//
Be sure to click the color state to trigger a re-render.
See how updating color
only triggers a self-update of <ColorPicker>
and only updates relevant parts of the UI now?
<App>
contains no state, so <p>
and <ExpensiveTree>
will never be re-rendered by <App>
.
memo
let’s you opt out of an owner-update by doing an extra check to see if any of the component’s props have
changed.
Basically, it turns the components props in to a dependency list for owner-updates.
These techniques [moving state down and using slotted components to lift UI] are complementary to what you already know! They don’t replace
memo
oruseMemo
, but they’re often good to try first.
Memoization is contagious.
Props passed to a memoized component need to be referentially equal across renders 5.
You need to apply useCallback
& useMemo
to all unstable props for memo
to work,
which might be a long chain of values up the tree.
That’s where it’s easy to go wrong, one unstable value can bust memo
.
In which case, React is doing more work than before, with nothing gained.
There’s a time and place for memo
but it’s a fickle fix.
Don’t be careful before it’s necessary.
Only optimise for rendering performance when it’s a measurable issue. Find chokepoints that you’re certain memo
can solve, then apply it.
Before you apply optimizations like
memo
oruseMemo
, it might make sense to look if you can split the parts that change from the parts that don’t change. The interesting part about these approaches is that they don’t really have anything to do with performance, per se. Using thechildren
prop to split up components usually makes the data flow of your application easier to follow and reduces the number of props plumbed down through the tree. Improved performance in cases like this is a cherry on top, not the end goal.
If you structure your components in such a way that the parts of the UI are aligned with the updates, the updates will be more granular and less work will be done.
You may never even need to use memo
.
A related and lesser-known pitfall is that slotted components don’t work with memo
7 8.
children
is not really different than any other prop, it’s passed-down data too.
The React element that is passed-down (the JSX that the component wraps) is recreated whenever the owner re-renders.
Remember, one new prop is enough for memo
to go ahead and re-render, so that busts it.
This is not a very common issue but easy to miss and not mentioned in the docs.
You can get around it by memoizing the child element from the owner’s side with useMemo
.
function User({ name, data, here }) { const content = useMemo(() => <p>Hello, {name}.</p>, [name]); return ( <SlottedMemo more={data} passed={here}> {content} </SlottedMemo> );}
Dual-memoization!
Slotted components are decoupled from their content. Last post, we saw that how that can make data flow easier to follow. This post, we’ve seen how slotted components can be leveraged to avoid re-renders in parts of the UI that don’t need to be updated.
A component and the tree it owns can be viewed as a unit-of-update. Slotted components make the tree a component owns smaller and therefore the units-of-update smaller too. If state/updates are aligned with relevant parts of the UI, less work is done re-rendering and improved performance can come naturally.
Thanks to Dan Abramov for the example.
- Before you memo — Overreacted
- One simple trick to optimize React re-renders
- Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior
-
↩
After you trigger a render, React calls your components to figure out what to display on screen. “Rendering” is React calling your components. On initial render, React will call the root component. For subsequent renders, React will call the function component whose state update triggered the render.
React Docs: Render and Commit — Step 2: React renders your components
This process is recursive: if the updated component returns some other component, React will render that component next, and if that component also returns something, it will render that component next, and so on. The process will continue until there are no more nested components and React knows exactly what should be displayed on screen. -
↩
There’s also a lesser-known technique as well: if a React component returns the exact same element reference in its render output as it did the last time, React will skip re-rendering that particular child. There's at least a couple ways to implement this technique: If you include
Mark's Dev Blog: Blogged Answers A (Mostly) Complete Guide to React Rendering Behavior - Component Render Optimization Techniquesprops.children
in your output, that element is the same if this component does a state update If you wrap some elements withuseMemo()
, those will stay the same until the dependencies change. -
↩
This is a little known React optimization pattern. React re-renders this [slotted] component deeply when [the state updater] is called, and since
Sebastian Markbåge tweetchildren
can't have changed, it automatically bails out of trying to update the children. No need for [...]memo
, etc. -
↩
[Passing down elements is] both natural for composition and acts as an optimization opportunity.
Dan Abramov Tweet -
↩
Optimizing with
React Docs: memo - Should you add memo everywhere?memo
is only valuable when your component re-renders often with the same exact props, and its re-rendering logic is expensive. If there is no perceptible lag when your component re-renders,memo
is unnecessary. Keep in mind thatmemo
is completely useless if the prop passed to your component are always different, such as if you pass an object or a plain function defined during rendering. This is why you will often needuseMemo
anduseCallback
together withmemo
. -
↩
Conceptually, we could say that the difference between these two approaches is:
Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior - Component Render Optimization TechniquesReact.memo()
: controlled by the child component. Same-element references: controlled by the parent component -
↩
Avoid using
Brandon Dail TweetReact.memo
on components that useprops.children
! Those children will almost certainly change every render, soReact.memo
will never prevent the component from re-rendering, making it unnecessary overhead ✨. Demo: CodeSandbox -
↩
It’s an obvious issue in retrospect:
Why using the children prop makes React.memo() not work - Reinis Ivanovs (slikts)React.memo()
shallowly compares the new and the old props and short-circuits the render lifecycle if they’re the same, and thechildren
prop isn’t special, so passing newly created React elements (so any JSX that isn’t specifically persisted) aschildren
will cause a re-render. However, it’s not always easy to connect the dots in practice; for example, I arrived at this issue when I addedchildren
to a previously memoized component and noticed that it would re-render unexpectedly.