Performance is a core factor in any successful web apps, Users won't bother to wait over 2 seconds for websites to finish loading, and because of that you -the web developer- should make sure that your websites doesn't exceed that.
In this tutorial, We're going to take a look at ReactJS rendering cycle, how does it work, what trigger a render, how do we optimize it using methods such as: useMemo
and useCallback
, and when we should (or shouldn't) use them.
Table of Contents
- What is rendering
- The ReactJS rendering cycle
- What triggers a re-render?
- Memoization techniques
- You're trading time with memory!
- Further Reading
- Conclusion
What is rendering
Simply means that ReactJS draws the shape (UI) of a component based on its states and props. It does that by converting the component code into html elements and inject it into the DOM.
The ReactJS rendering cycle
When it comes to converting a component code into html elements ReactJS split this process into two steps: the render step and the commit step. however this process behavior is slightly different depending on if the components are initialized or re-rendered
When initializing the component
the render step
Here React will traverse the DOM from the root element to all it's descendants, while traversing React will convert JSX into a React Element using the createElement() methodthe commit step
here all React Elements are applied to the DOM
When re-rendering a component
React will traverse the DOM in search of elements that are flagged for update. once finished it will proceed to the render step
the render step
Similarly to initializing the component, React will convert JSX into a React Element for each element flagged for update using the createElement() method. Then they'll be compared to the their last render creating a list of all changes that needs to be made in the DOMthe commit step
Finally all elements that are different from themselves in the last render will be applied to the DOM, Otherwise React will discard the operation
sometimes the rendering step can be slow (ex: component is too big), which causes performance hits, therefore if we need to eliminate those performance hits we should remove unnecessary re-renders. Next we'll discuss what triggers a re-render.
What triggers a re-render?
There are 3 main factors that cause a re-render
The component state has changed
You've probably knows this, Changing the state of a component (either using useState
or useReducer
) will cause a re-render so that the website UI match the component, Here is a basic example
import {useState, useEffect} from "react"
function APP() {
const [count, setCount] = useState(0)
useEffect(() => console.log('new render'), [])
return <button onClick={() => setState(count + 1)}>click me</button>
}
each time you click on the button will fire a console log implying a re-render.
Speaking of state hooks, You may find this interesting:
useState vs useReducer: What are they and when to use them?
m0nm ・ Feb 19 '22
The component parent is re-rendered
If the parent component triggered a re-render (either by these 4 factors) the component will also re-render, Example
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setState(count + 1)}>click me</button>
<Child />
</div>
)
}
function Child() {
useEffect(() => {
console.log("I'm being re-rendered because of my parent 😠")
}, [])
return <h1>I'm a child component</h1>
}
each time you click on the button it will fire a console log with the message I'm being re-rendered because of my parent 😠
, implying a re-render of the child component.
Context value has changed
if the component is consuming context provider value then any change in the value will re-render the component
import {useState,createContext, useContext} from "react"
const CountContext = createContext(null)
function App() {
const [count, setCount] = useState(1);
const onClick = () => {
setCount(prev => prev + 1);
};
<CountContext.Provider value={count}>
<Component />
</CountContext.Provider>
}
function Component() {
const value = useContext(CountContext)
console.log("re-render")
return <h1>count is: {value}</h1>
}
What about props change?
Props are passed from parent to child, In most cases props are a state in the parent component, When props changes that means the parent's state has changed which causes the parent to re-render, which means the child will re-render as well as stated above
Memoization techniques
Now we're going to explore memo
, useMemo
and useCallback
, how they work and how to use them
memo
when we have a Parent
component and Child
component with it's props. We know that when the parent component re-renders then all of it's descendants will re-render as well, Even if their props hasn't changed. The reason is that React compares the old props with the new props which do not reference the same object, therefore React will re-render the child component.
If it happens to be a problem to you, You can use React higher order component memo
to memoize the Child
component:
// ./Child.jsx
import {memo} from "react"
function Child({name}) {
console.log("not gonna re-render when my parent do")
return <h1>My memoized name is: {name}</h1>
}
export default memo(Child)
// ./Parent.jsx
function Parent() {
const [count, setCount] = useState(1);
const name = "john"
return(
<button onClick={() => setState(count + 1)}>click me</button>
<Child name={name} />
)
}
Composition
Another trick you could use without relying on memo
is composition, more about it by Kent Dodds
function Child({name}) {
console.log("not gonna re-render when my parent do")
return <h1>My memoized name is: {name}</h1>
}
// ./Parent.jsx
function Parent({children}) {
const [count, setCount] = useState(1);
return(
<button onClick={() => setState(count + 1)}>click me</button>
{children}
)
}
function App() {
const name = "john"
return (
<Parent>
<Child name={name} />
</Parent>
)
}
Using component's props
You can achieve the same by passing the Child
as a prop to Parent
function Child({name}) {
console.log("not gonna re-render when my parent do")
return <h1>My memoized name is: {name}</h1>
}
// ./Parent.jsx
function Parent({child}) {
const [count, setCount] = useState(1);
return(
<button onClick={() => setState(count + 1)}>click me</button>
{child}
)
}
function App() {
const name = "john"
return (
<Parent child={<Child name={name} />} />
)
}
useMemo
When React re-renders every function within the component will be re-created again, this could cause performance issues if the function is expensive. In order to fix that useMemo
allows you to memoize expensive functions so that you can avoid calling them on every render.
You can see below we have expensiveFunction
that is slow, by passing it to useMemo
we make sure that it's not called again unless we change the bool
state, so that when we click on "increment count" button there won't be any lag:
import {useState, useMemo} from "react"
function App() {
const [count, setCount] = useState(1);
const [bool, setBool] = useState(false);
const expensiveFunction = useMemo(() => {
for(let i = 0; i < 100000000; i++) {}
// do something
return bool
} , [bool])
return (
// increment count
<button onClick={() => setCount(count + 1)}>increment count</button>
// re render to use expensiveFunction
<button onClick={() => setBool(!bool)}>re-render</button>
)
}
useCallback
The useCallback
and useMemo
Hooks are similar. The main difference is that useMemo
returns a memoized value and useCallback
returns a memoized function.
import {useState, useCallback} from "react"
function App() {
const [count, setCount] = useState(1);
const [bool, setBool] = useState(false);
const expensiveFunction = useCallback(() => {
for(let i = 0; i < 100000000; i++) {}
// do something
} , [bool])
return (
// increment count
<button onClick={() => setCount(count + 1)}>increment count</button>
// re render to use expensiveFunction
<button onClick={() => setBool(!bool)}>re-render</button>
)
}
You're trading time with memory!
Now this doesn't mean to use these memoizing hooks in every piece of code of your app, Know that what you're doing is trading time with memory. Too much memory usage is just going to make it worse.
So when should you use them?
Only when you notice that your app is slow when re-rendering, Otherwise don't touch the code: "If it's not broken don't fix it"
Further Reading
If you want to go in depth about this topic, I recommend you read Mark Erikson post:
A (Mostly) Complete Guide to React Rendering Behavior
Conclusion
We've reached the end of this post, I hope it was helpful to you, If it was then please consider liking this post. If you have any questions just hit me up in the comment section, I'm not an expert in this subject but I'll try my best.
Top comments (0)