Understanding how React renders our applications is crucial for any React developer. Yet it‘s also one of the most perplexing topics for React newbies and even experienced developers.
In this comprehensive guide, you‘ll learn how React‘s rendering really works behind the scenes. You‘ll understand when and why re-rendering occurs, how to optimize performance, and gain key insights from real-world examples.
By the end, you‘ll have the expertise to build blazing fast React apps that render efficiently. Sound exciting? Let‘s dive in!
What is "Rendering" in React?
Before jumping into React rendering, let‘s first build a solid mental model of what "rendering" really means.
The Virtual DOM – React‘s Secret Sauce
React uses a virtual representation of the actual DOM called the virtual DOM. Instead of manipulating the actual DOM directly, React generates a virtual DOM made up of React elements.
These React elements are just plain JavaScript objects that describe what the DOM should look like.
For example, this JSX:
<div className="app">
<p>This is some text</p>
</div>
Gets converted to:
{
type: ‘div‘,
props: {
className: ‘app‘,
children: [
{
type: ‘h1‘,
props: {
children: ‘Hello World‘
}
},
{
type: ‘p‘,
props: {
children: ‘This is some text‘
}
}
]
}
}
So the virtual DOM is a tree of these React element objects. This allows React to do comparisons on these objects to determine what needs to change in the real DOM.
This innovation is what enables React‘s performance since the virtual DOM can update efficiently without touching the real DOM.
The virtual DOM is like a virtual representation of the UI that React can update quickly without modifying the actual DOM.
What Does "Rendering" Refer to?
In React, rendering refers to the process of generating the virtual DOM for a component tree.
More specifically, rendering in React involves:
- Whenever a component gets initialized, React will "render" it – meaning it generates the virtual DOM React elements representing its UI
- This rendering process is recursive, so React will render the component‘s children also
- Once the virtual DOM has been created, React can now diff to determine necessary DOM updates
So in summary:
- Rendering ≠ updating the real DOM. It refers to building up the virtual DOM representation.
- Components get re-rendered whenever their state changes.
- Re-rendering generates a new virtual DOM tree.
- After re-rendering, React compares the new virtual DOM with the previous one.
Rendering creates the virtual DOM representation of your React components.
Now that we understand what rendering is, let‘s go over when re-rendering happens.
When Does Re-rendering Occur in React?
Re-rendering is a normal part of React. But excessive re-rendering can hurt performance, so it‘s vital to understand what causes it.
Let‘s go over the common cases where components re-render unnecessarily.
1. Parent Components Re-rendering
By default, whenever a parent component re-renders, React will re-render all its child components recursively.
function Parent() {
// re-renders on some state change
}
function Child() {
// re-renders even though no changes
}
// Parent renders
// Child renders even though unchanged
<Parent>
<Child />
</Parent>
This leads to a lot of unnecessary re-rendering. A small state change in Parent forces Child to re-render even if its props are unchanged.
In fact, amajor performance problem in large React apps is unnecessary re-rendering due to parent updates.
Parent re-rendering causes child components to re-render unnecessarily.
2. Changes in Context Values
When using React Context, components re-render whenever the context value changes:
const ThemeContext = React.createContext(‘dark‘)
function App() {
return (
<ThemeContext.Provider value={‘light‘}>
<Page />
</ThemeContext.Provider>
)
}
function Page() {
// re-renders even though it doesn‘t read ThemeContext
}
Here, Page will re-render even if it doesn‘t actually use the theme context value.
Context changes can force deep re-rendering even if the component doesn‘t consume that context.
Context value changes lead to re-renders of consumer components.
3. Hooks Like useEffect
Hooks like useEffect cause a component to re-render after executing:
function MyComponent() {
useEffect(() => {
// do something
})
return <div>Hi</div>
}
Here, MyComponent will re-render after the effect runs because effects are cleaned up and re-run after re-rendering.
While sometimes necessary, re-rendering due to effects can also happen unnecessarily.
The
useEffecthook causes components to re-render after execution.
There are also other cases like useState and useReducer re-rendering components when updater functions run. But in general, the main sources of excessive re-rendering boil down to parents, contexts, and hooks.
Real-World Examples of Wasteful Re-rendering
To make this more concrete, let‘s go through some real-world examples of unnecessary re-rendering.
Scenario 1: Re-rendering Long Lists
Here is a component that renders a long list of items:
function ItemList() {
// fetch list of items
const [items] = useState(fetchItems())
return (
<ul>
{items.map(item => <Item key={item.id} item={item} />)}
</ul>
)
}
This seems fine. But now let‘s say somewhere up the component tree, there‘s a context update:
function MainLayout() {
const [theme, setTheme] = useState(‘dark‘)
return (
<ThemeContext.Provider value={theme}>
<ItemList />
</ThemeContext.Provider>
)
}
Although the ItemList doesn‘t read the ThemeContext, this context update will still cause ItemList and all its Item children to re-render!
Even though the list is unchanged, React will re-render potentially hundreds of items. This can severely affect performance with long lists.
Context updates can re-render huge component subtrees even if context isn‘t consumed.
Scenario 2: Re-rendering Expensive Calculations
Another common example is re-computing expensive values needlessly:
function MyComponent({ data }) {
const [expensiveValue, setExpensiveValue] = useState(() => {
return heavyCalculation(data)
})
// ...
}
Here, every time MyComponent renders, we re-compute expensiveValue via an expensive calculation.
This is wasteful if data hasn‘t changed. But React will still call heavyCalculation on each render by default.
Expensive calculations can get re-run unnecessarily on every render.
These are just a couple common real-world examples of how excessive re-rendering affects performance. The key takeaway is that re-rendering can happen frequently without us realizing it.
But there are patterns we can use to optimize this.
Optimizing Rendering Performance in React Apps
Let‘s now go over actionable strategies to optimize rendering performance.
1. React.memo for Component Memoization
React.memo allows us to easily memoize function components:
const MyComponent = React.memo(function MyComponent(props) {
// only rerenders if props change
})
React.memo does a shallow comparison on props to determine if a re-render is needed.
This can prevent unnecessary re-renders of UI components like ListItems whose rendering depends solely on props.
React.memomemoizes components to prevent unnecessary re-renders.
However, React.memo has a couple caveats:
- It only does a shallow comparison on props. For deep comparisons, use
useMemo - It still renders when parent components render. To fix this, also use
useMemo.
2. useMemo for Expensive Calculations
useMemo allows us to memoize expensive calculations:
const value = useMemo(() => heavyComputation(a, b), [a, b])
This ensures heavyComputation is only re-run if a or b change.
useMemo takes a function and an array of inputs. The computation is memoized and only re-runs when the inputs change.
useMemoprevents expensive computations from re-running unnecessarily.
3. useCallback to Memoize Callback Functions
Similarly, useCallback memoizes callback functions between renders:
const memoizedHandleClick = useCallback(
() => {
// handle click
},
[]
)
Now memoizedHandleClick will not change reference between re-renders.
This avoids passing callbacks down to optimized child components like React.memo unnecessarily.
useCallbackmemoizes callback functions to prevent passing new function references unnecessarily.
4. Virtualize Long Lists with react-window
As we saw earlier, large lists can force components to re-render unnecessarily.
Virtualization libraries like react-window solve this by only rendering visible items:
function VirtualizedList() {
// only renders visible rows
return (
<List
height={600}
itemCount={10000}
itemSize={50}
>
{Item}
</List>
)
}
This ensures long lists only render what‘s visible.
Virtualization renders only visible data to optimize long lists.
5. useReducer for State Composition
useReducer can help optimize re-rendering by managing state in reducers:
function reducer(state, action) {
switch(action.type) {
// update counters
case ‘increment‘:
return {
...state,
count: state.count + 1
}
// don‘t update state
case ‘setTheme‘:
return state
default:
return state
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
const { count, theme } = state
// increment will only update count
const increment = () => dispatch({ type: ‘increment‘ })
// set theme only updates theme
const setTheme = (theme) => dispatch({ type: ‘setTheme‘, theme })
return /* ... */
}
Here, components that depend on count won‘t re-render if theme changes since state is separated.
useReducerallows optimizing state updates with reducers.
6. useIsomorphicLayoutEffect to Avoid Re-rendering Effects
The useLayoutEffect hook used for DOM mutations unfortunately always causes a re-render.
But the less known useIsomorphicLayoutEffect hook avoids this:
function MyComponent() {
useIsomorphicLayoutEffect(() => {
// mutates DOM
})
return <div>Hi</div>
}
useIsomorphicLayoutEffect acts like useLayoutEffect but doesn‘t force a render after running.
This prevents unnecessary re-rendering when using effects for DOM mutations.
useIsomorphicLayoutEffectprevents re-rendering after DOM mutations.
These are some of the powerful techniques for optimizing rendering performance.
Putting it All Together: A Case Study
Let‘s now see how we can apply these patterns to optimize a real-world app.
Imagine we have a music playlist dashboard that displays:
- A list of playlists
- The songs in the current playlist
- The currently playing song
When the play button is clicked, the current song changes.
We want to avoid unnecessary re-rendering as interactions occur.
First, we can React.memo our SongList and SongItem components since they are reusable:
const SongList = ({ songs }) => {
return (
<ul>
{songs.map(song => <SongItem song={song} />)}
</ul>
)
}
export const SongItem = React.memo(function SongItem({ song }) {
return <li>{song.title}</li>
})
Next, we can useMemo the current song since it can be derived from state:
function Playlist() {
const [songs] = useState(fetchSongs())
const [currentSongIndex, setCurrentSongIndex] = useState(0)
const currentSong = useMemo(() => {
return songs[currentSongIndex]
}, [currentSongIndex, songs])
// ...
}
This prevents re-computing currentSong unnecessarily.
We can also extract the dispatch logic into a useReducer reducer:
function playlistReducer(state, action) {
switch(action.type) {
case ‘PLAY_NEXT‘: {
return {
...state,
currentSongIndex: state.currentSongIndex + 1
}
}
default:
return state
}
}
function Playlist() {
const [state, dispatch] = useReducer(playlistReducer, {
songs: [],
currentSongIndex: 0
})
const {currentSongIndex, songs} = state
const handlePlay = () => {
dispatch({ type: ‘PLAY_NEXT‘ })
}
// ...
}
This ensures only currentSongIndex updates when the play button is clicked, avoiding re-rendering the rest of the UI.
Finally, we can wrap our Playlist in React.memo as well:
export const Playlist = React.memo(Playlist)
Now only necessary components re-render as interactions occur.
By following patterns like:
- Memoization with
React.memoand hooks likeuseMemo - Optimizing state with
useReducer - Virtualization of long lists
We can optimize rendering performance.
Key Takeaways on Optimizing Rendering
Here are the big ideas:
- Rendering refers to React generating the virtual DOM representation of components
- Excessive re-rendering is caused by parent updates, context changes, hooks, and more
- Techniques like
React.memo,useMemo,useCallbackmemoize components and data useReducerand state composition optimize which state changes cause re-rendersreact-windowvirtualizes long lists to prevent re-rendering offscreen items- Tools like React Profiler provide low-level insights into component rendering
While it takes time to master, understanding React rendering will level up your ability to build high-performance React applications.
Wrapping Up
We covered a lot of ground here! Here are the key takeaways:
-
Rendering refers to React generating the virtual DOM representation of your components. It does not mean updating the real DOM.
-
Excessive re-rendering is often caused by parent component updates, context value changes, hooks scheduling updates, and more.
-
Techniques like
React.memo,useMemo, anduseCallbackcan optimize performance by memoizing data to prevent unnecessary re-renders. -
Virtualizing long lists prevents offscreen items from rendering.
-
The React Profiler provides low-level insights into component rendering performance.
-
Applying patterns like memoization, reducer composition, and virtualization can optimize real-world apps.
I hope this guide gave you a deep understanding into React rendering behavior and patterns for optimization. Mastering these concepts takes time but will give you the skills to build high-performance React apps that render efficiently.
Let me know if you have any other questions! I‘m always happy to chat more about React performance.