Originally published at https://www.developerway.com. The website has more articles like this đ
Performance and React! Such a fun topic with so many controversial opinions and so many best practices flipping to be the opposite in just 6 months. Is it even possible to say anything definitive here or to make any generalized recommendations?
Usually, performance experts are the proponents of âpremature optimisation is the root of all evilâ and âmeasure firstâ rules. Which loosely translates into âdonât fix that is not brokenâ and is quite hard to argue with. But Iâm going to anyway đ
What I like about React, is that it makes implementing complicated UI interactions incredibly easy. What I donât like about React, is that it also makes it incredibly easy to make mistakes with huge consequences that are not visible right away. The good news is, itâs also incredibly easy to prevent those mistakes and write code that is performant most of the time right away, thus significantly reducing the time and effort it takes to investigate performance problems since there will be much fewer of those. Basically, âpremature optimisationâ, when it comes to React and performance, can actually be a good thing and something that everyone should do đ. You just need to know a few patterns to watch out for in order to do that meaningfully.
So this is exactly what I want to prove in this article đ. Iâm going to do that by implementing a âreal-lifeâ app step by step, first in a ânormalâ way, using the patterns that youâll see practically everywhere and surely used multiple times by yourself. And then refactor each step with performance in mind, and extract a generalized rule from every step that can be applied to most apps most of the time. And then compare the result in the end.
Letâs begin!
We are going to write one of the âsettingsâ page for an online shop (that we introduced into the previous âAdvanced typescript for React developersâ articles). On this page, users will be able to select a country from the list, see all the information available for this country (like currency, delivery methods, etc), and then save this country as their country of choice. The page would look something like this:
On the left weâll have a list of countries, with âsavedâ and âselectedâ states, when an item in the list is clicked, in the column on the right the detailed information is shown. When the âsaveâ button is pressed, the âselectedâ country becomes âsavedâ, with the different item colour.
Oh, and weâd want the dark mode there of course, itâs 2022 after all!
Also, considering that in 90% of the cases performance problems in React can be summarised as âtoo many re-rendersâ, we are going to focus mostly on reducing those in the article. (Another 10% are: ârenders are too heavyâ and âreally weird stuff that need further investigationâ.)
Let's structure our app first
First of all, let's take a look at the design, draw imaginary boundaries, and draft the structure of our future app and which components weâd need to implement there:
- a root âPageâ component, where weâd handle the âsubmitâ logic and country selection logic
- a âList of countriesâ component, that would render all the countries in a list, and in the future handle things like filtering and sorting
- âItemâ component, that renders the country in the âList of countriesâ
- a âSelected countryâ component, that renders detailed information about the selected country and has the âSaveâ button
This is, of course, not the only possible way to implement this page, thatâs the beauty and the curse of React: everything can be implemented in a million ways and there is no right or wrong answer for anything. But there are some patterns that in the long run in fast-growing or large already apps can definitely be called ânever do thisâ or âthis is a must-haveâ.
Letâs see whether we can figure them out together đ
Implementing Page component
Now, finally, the time to get our hands dirty and do some coding. Letâs start from the ârootâ and implement the Page component.
First: we need a wrapper with some styles that renders page title, âList of countriesâ and âSelected countryâ components.
Second: out page should receive the list of countries from somewhere, and then pass it to the CountriesList
component so that it could render those.
Third: our page should have an idea of a âselectedâ country, that will be received from the CountriesList
component and passed to the SelectedCountry
component.
And finally: our page should have an idea of a âsavedâ country, that will be received from the SelectedCountry
component and passed to the CountriesList
component (and be sent to the backend in the future).
export const Page = ({ countries }: { countries: Country[] }) => {
const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0]);
const [savedCountry, setSavedCountry] = useState<Country>(countries[0]);
return (
<>
<h1>Country settings</h1>
<div css={contentCss}>
<CountriesList
countries={countries}
onCountryChanged={(c) => setSelectedCountry(c)}
savedCountry={savedCountry}
/>
<SelectedCountry
country={selectedCountry}
onCountrySaved={() => setSavedCountry(selectedCountry)}
/>
</div>
</>
);
};
That is the entire implementation of the âPageâ component, itâs the most basic React that you can see everywhere, and there is absolutely nothing criminal in this implementation. Except for one thing. Curious, can you see it?
Refactoring Page component - with performance in mind
I think it is common knowledge by now, that react re-renders components when there is a state or props change. In our Page component when setSelectedCountry
or setSavedCountry
is called, it will re-render. If the countries array (props) in our Page component changes, it will re-render. And the same goes for CountriesList
and SelectedCountry
components - when any of their props change, they will re-render.
Also, anyone, who spent some time with React, knows about javascript equality comparison, the fact that React does strict equality comparison for props, and the fact that inline functions create new value every time. This leads to the very common (and absolutely wrong btw) belief, that in order to reduce re-renders of CountriesList
and SelectedCountry
components we need to get rid of re-creating inline functions on every render by wrapping inline functions in useCallback
. Even React docs mention useCallback
in the same sentence with âprevent unnecessary rendersâ! See whether this pattern looks familiar:
export const Page = ({ countries }: { countries: Country[] }) => {
// ... same as before
const onCountryChanged = useCallback((c) => setSelectedCountry(c), []);
const onCountrySaved = useCallback(() => setSavedCountry(selectedCountry), []);
return (
<>
...
<CountriesList
onCountryChanged={onCountryChange}
/>
<SelectedCountry
onCountrySaved={onCountrySaved}
/>
...
</>
);
};
Do you know the funniest part about it? It actually doesnât work. Because it doesnât take into account the third reason why React components are re-rendered: when the parent component is re-rendered. Regardless of the props, CountriesList
will always re-render if Page is re-rendered, even if it doesnât have any props at all.
We can simplify the Page example into just this:
const CountriesList = () => {
console.log("Re-render!!!!!");
return <div>countries list, always re-renders</div>;
};
export const Page = ({ countries }: { countries: Country[] }) => {
const [counter, setCounter] = useState<number>(1);
return (
<>
<h1>Country settings</h1>
<button onClick={() => setCounter(counter + 1)}>
Click here to re-render Countries list (open the console) {counter}
</button>
<CountriesList />
</>
);
};
And every time we click the button, weâll see that CountriesList
is re-rendered, even if it doesnât have any props at all. Codesandbox code is here.
And this, finally, allows us to solidify the very first rule of this article:
Rule #1. If the only reason you want to extract your inline functions in props into useCallback is to avoid re-renders of children
components: donât. It doesnât work.
Now, there are a few ways to deal with situations like the above, I am going to use the simplest one for this particular occasion: useMemo hook. What it does is itâs essentially âcachesâ the results of whatever function you pass into it, and only refreshes them when a dependency of useMemo
is changed. If I just extract the rendered CountriesList
into a variable const list = <ComponentList />;
and then apply useMemo
on it, the ComponentList
component now will be re-rendered only when useMemo dependencies will change.
export const Page = ({ countries }: { countries: Country[] }) => {
const [counter, setCounter] = useState<number>(1);
const list = useMemo(() => {
return <CountriesList />;
}, []);
return (
<>
<h1>Country settings</h1>
<button onClick={() => setCounter(counter + 1)}>
Click here to re-render Countries list (open the console) {counter}
</button>
{list}
</>
);
};
Which in this case is never, since it doesnât have any dependencies. This pattern basically allows me to break out of this âparent re-renders - re-render all the children regardlessâ loop and take control over it. Check out the full example in codesandbox.
The most important thing there to be mindful of is the list of dependencies of useMemo
. If it depends on exactly the same thing that causes the parent component to re-render, then itâs going to refresh its cache with every re-render, and essentially becomes useless. For example, if in this simplified example I pass the counter
value as a dependency to the list
variable (notice: not even a prop to the memoised component!), that will cause useMemo
to refresh itself with every state change and will make CountriesList
re-render again.
const list = useMemo(() => {
return (
<>
{counter}
<CountriesList />
</>
);
}, [counter]);
Okay, so all of this is great, but how exactly it can be applied to our non-simplified Page component? Well, if we look closely to its implementation again
export const Page = ({ countries }: { countries: Country[] }) => {
const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0]);
const [savedCountry, setSavedCountry] = useState<Country>(countries[0]);
return (
<>
<h1>Country settings</h1>
<div css={contentCss}>
<CountriesList
countries={countries}
onCountryChanged={(c) => setSelectedCountry(c)}
savedCountry={savedCountry}
/>
<SelectedCountry
country={selectedCountry}
onCountrySaved={() => setSavedCountry(selectedCountry)}
/>
</div>
</>
);
};
weâll see that:
-
selectedCountry
state is never used inCountriesList
component -
savedCountry
state is never used inSelectedCountry
component
Which means that when selectedCountry
state changes, CountriesList
component doesnât need to re-render at all! And the same story with savedCountry
state and SelectedCountry
component. And I can just extract both of them to variables and memoise them to prevent their unnecessary re-renders:
export const Page = ({ countries }: { countries: Country[] }) => {
const [selectedCountry, setSelectedCountry] = useState<Country>(countries[0]);
const [savedCountry, setSavedCountry] = useState<Country>(countries[0]);
const list = useMemo(() => {
return (
<CountriesList
countries={countries}
onCountryChanged={(c) => setSelectedCountry(c)}
savedCountry={savedCountry}
/>
);
}, [savedCountry, countries]);
const selected = useMemo(() => {
return (
<SelectedCountry
country={selectedCountry}
onCountrySaved={() => setSavedCountry(selectedCountry)}
/>
);
}, [selectedCountry]);
return (
<>
<h1>Country settings</h1>
<div css={contentCss}>
{list}
{selected}
</div>
</>
);
};
And this, finally, lets us formalize the second rule of this article:
Rule #2. If your component manages state, find parts of the render tree that donât depend on the changed state and memoise them to
minimize their re-renders.
Implementing the list of countries
Now, that our Page component is ready and perfect, time to flesh out its children. First, letâs implement the complicated component: CountriesList
. We already know, that this component should accept the list of countries, should trigger onCountryChanged
callback when a country is selected in the list, and should highlight the savedCountry
into a different color, according to design. So letâs start with the simplest approach:
type CountriesListProps = {
countries: Country[];
onCountryChanged: (country: Country) => void;
savedCountry: Country;
};
export const CountriesList = ({
countries,
onCountryChanged,
savedCountry
}: CountriesListProps) => {
const Item = ({ country }: { country: Country }) => {
// different className based on whether this item is "saved" or not
const className = savedCountry.id === country.id ? "country-item saved" : "country-item";
// when the item is clicked - trigger the callback from props with the correct country in the arguments
const onItemClick = () => onCountryChanged(country);
return (
<button className={className} onClick={onItemClick}>
<img src={country.flagUrl} />
<span>{country.name}</span>
</button>
);
};
return (
<div>
{countries.map((country) => (
<Item country={country} key={country.id} />
))}
</div>
);
};
Again, the simplest component ever, only 2 things are happening there, really:
- we generate the
Item
based on the props we receive (it depends on bothonCountryChanged
andsavedCountry
) - we render that
Item
for all countries in a loop
And again, there is nothing criminal about any of this per se, I have seen this pattern used pretty much everywhere.
Refactoring List of countries component - with performance in mind
Time again to refresh a bit our knowledge of how React renders things, this time - what will happen if a component, like Item
component from above, is created during another component render? Short answer - nothing good, really. From Reactâs perspective, this Item
is just a function that is new on every render, and that returns a new result on every render. So what it will do, is on every render it will re-create results of this function from scratch, i.e. it will just compare the previous component state with the current one, like it happens during normal re-render. It will drop the previously generated component, including its DOM tree, remove it from the page, and will generate and mount a completely new component, with a completely new DOM tree every single time the parent component is re-rendered.
If we simplify the countries example to demonstrate this effect, it will be something like this:
const CountriesList = ({ countries }: { countries: Country[] }) => {
const Item = ({ country }: { country: Country }) => {
useEffect(() => {
console.log("Mounted!");
}, []);
console.log("Render");
return <div>{country.name}</div>;
};
return (
<>
{countries.map((country) => (
<Item country={country} />
))}
</>
);
};
This is the heaviest operation from them all in React. 10 ânormalâ re-renders is nothing compared to the full re-mounting of a freshly created component from a performance perspective. In normal circumstances, useEffect
with an empty dependencies array would be triggered only once - after the component finished its mounting and very first rendering. After that the light-weight re-rendering process in React kicks in, and component is not created from scratch, but only updated when needed (thatâs what makes React so fast btw). Not in this scenario though - take a look at this codesandbox, click on the âre-renderâ button with open console, and enjoy 250 renders AND mountings happening on every click.
The fix for this is obvious and easy: we just need to move the Item
component outside of the render function.
const Item = ({ country }: { country: Country }) => {
useEffect(() => {
console.log("Mounted!");
}, []);
console.log("Render");
return <div>{country.name}</div>;
};
const CountriesList = ({ countries }: { countries: Country[] }) => {
return (
<>
{countries.map((country) => (
<Item country={country} />
))}
</>
);
};
Now in our simplified codesandbox mounting doesnât happen on every re-render of the parent component.
As a bonus, refactoring like this helps maintain healthy boundaries between different components and keep the code cleaner and more concise. This is going to be especially visible when we apply this improvement to our ârealâ app. Before:
export const CountriesList = ({
countries,
onCountryChanged,
savedCountry
}: CountriesListProps) => {
// only "country" in props
const Item = ({ country }: { country: Country }) => {
// ... same code
};
return (
<div>
{countries.map((country) => (
<Item country={country} key={country.id} />
))}
</div>
);
};
After:
type ItemProps = {
country: Country;
savedCountry: Country;
onItemClick: () => void;
};
// turned out savedCountry and onItemClick were also used
// but it was not obvious at all in the previous implementation
const Item = ({ country, savedCountry, onItemClick }: ItemProps) => {
// ... same code
};
export const CountriesList = ({
countries,
onCountryChanged,
savedCountry
}: CountriesListProps) => {
return (
<div>
{countries.map((country) => (
<Item
country={country}
key={country.id}
savedCountry={savedCountry}
onItemClick={() => onCountryChanged(country)}
/>
))}
</div>
);
};
Now, that we got rid of the re-mounting of Item
component every time the parent component is re-rendered, we can extract the third rule of the article:
Rule #3. Never create new components inside the render function of another component.
Implementing selected country
Next step: the âselected countryâ component, which is going to be the shortest and the most boring part of the article, since there is nothing to show there really: itâs just a component that accepts a property and a callback, and renders a few strings:
const SelectedCountry = ({ country, onSaveCountry }: { country: Country; onSaveCountry: () => void }) => {
return (
<>
<ul>
<li>Country: {country.name}</li>
... // whatever country's information we're going to render
</ul>
<button onClick={onSaveCountry} type="button">Save</button>
</>
);
};
đ€·đœââïž Thatâs it! Itâs only here just to make the demo codesandbox more interesting đ
Final polish: theming
And now the final step: dark mode! Who doesnât love those? Considering that the current theme should be available in most components, passing it through props everywhere would be a nightmare, so React Context is the natural solution here.
Creating theme context first:
type Mode = 'light' | 'dark';
type Theme = { mode: Mode };
const ThemeContext = React.createContext<Theme>({ mode: 'light' });
const useTheme = () => {
return useContext(ThemeContext);
};
Adding context provider and the button to switch it to the Page component:
export const Page = ({ countries }: { countries: Country[] }) => {
// same as before
const [mode, setMode] = useState<Mode>("light");
return (
<ThemeContext.Provider value={{ mode }}>
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Toggle theme</button>
// the rest is the same as before
</ThemeContext.Provider>
)
}
And then using the context hook to color our buttons in the appropriate theme:
const Item = ({ country }: { country: Country }) => {
const { mode } = useTheme();
const className = `country-item ${mode === "dark" ? "dark" : ""}`;
// the rest is the same
}
Again, nothing criminal in this implementation, a very common pattern, especially for theming.
Refactoring theming - with performance in mind.
Before weâll be able to catch whatâs wrong with the implementation above, time to look into a fourth reason why a React component can be re-rendered, that often is forgotten: if a component uses context consumer, it will be re-rendered every time the context providerâs value is changed.
Remember our simplified example, where we memoised the render results to avoid their re-renders?
const Item = ({ country }: { country: Country }) => {
console.log("render");
return <div>{country.name}</div>;
};
const CountriesList = ({ countries }: { countries: Country[] }) => {
return (
<>
{countries.map((country) => (
<Item country={country} />
))}
</>
);
};
export const Page = ({ countries }: { countries: Country[] }) => {
const [counter, setCounter] = useState<number>(1);
const list = useMemo(() => <CountriesList countries={countries} />, [
countries
]);
return (
<>
<h1>Country settings</h1>
<button onClick={() => setCounter(counter + 1)}>
Click here to re-render Countries list (open the console) {counter}
</button>
{list}
</>
);
};
Page
component will re-render every time we click the button since it updates the state on every click. But CountriesList
is memoised and is independent of that state, so it wonât re-render, and as a result Item
component wonât re-render as well. See the codesandbox here.
Now, what will happen if I add the Theme context here? Provider in the Page
component:
export const Page = ({ countries }: { countries: Country[] }) => {
// everything else stays the same
// memoised list is still memoised
const list = useMemo(() => <CountriesList countries={countries} />, [
countries
]);
return (
<ThemeContext.Provider value={{ mode }}>
// same
</ThemeContext.Provider>
);
};
And context in the Item component:
const Item = ({ country }: { country: Country }) => {
const theme = useTheme();
console.log("render");
return <div>{country.name}</div>;
};
If they were just normal components and hooks, nothing wouldâve happened - Item
is not a child of Page
component, CountriesList
wonât re-render because of memoisation, so Item
wouldnât either. Except, in this case, itâs a Provider-consumer combination, so every time the value on the provider changes, all of the consumers will re-render. And since weâre passing new object to the value all the time, Items
will unnecessary re-render on every counter. Context basically bypasses the memorisation we did and makes it pretty much useless. See the codesandbox.
The fix to it, as you might have already guessed, is just to make sure that the value
in the provider doesnât change more than it needs to. In our case, we just need to memoise it as well:
export const Page = ({ countries }: { countries: Country[] }) => {
// everything else stays the same
// memoising the object!
const theme = useMemo(() => ({ mode }), [mode]);
return (
<ThemeContext.Provider value={theme}>
// same
</ThemeContext.Provider>
);
};
And now the counter will work without causing all the Items to re-render!
And absolutely the same solution for preventing unnecessary re-renders we can apply to our non-simplified Page
component:
export const Page = ({ countries }: { countries: Country[] }) => {
// same as before
const [mode, setMode] = useState<Mode>("light");
// memoising the object!
const theme = useMemo(() => ({ mode }), [mode]);
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Toggle theme</button>
// the rest is the same as before
</ThemeContext.Provider>
)
}
And extract the new knowledge into the final rule of this article:
Rule #4: When using context, make sure that value property is always memoised if itâs not a number, string or boolean.
Bringing it all together
And finally, our app is complete! The entire implementation is available in this codesandbox. Throttle your CPU if youâre on the latest MacBook, to experience the world as the usual customers are, and try to select between different countries on the list. Even with 6x CPU reduction, itâs still blazing fast! đ
And now, the big question that I suspect many people have the urge to ask: âBut Nadia, React is blazing fast by itself anyway. Surely those âoptimisationsâ that you did wonât make much of a difference on a simple list of just 250 items? Arenât you exaggerating the importance here?â.
Yeah, when I just started this article, I also thought so. But then I implemented that app in the ânon-performantâ way. Check it out in the codesandbox. I donât even need to reduce the CPU to see the delay between selecting the items đ±. Reduce it by 6x, and itâs probably the slowest simple list on the planet that doesnât even work properly (it has a focus bug that the âperformantâ app doesnât have). And I havenât even done anything outrageously and obviously evil there! đ
So letâs refresh when React components re-render:
- when props or state have changed
- when parent component re-renders
- when a component uses context and the value of its provider changes
And the rules we extracted:
Rule #1: If the only reason why you want to extract your inline functions in props into useCallback
is to avoid re-renders of children components: donât. It doesnât work.
Rule #2: If your component manages state, find parts of the render tree that donât depend on the changed state and memoise them to minimize their re-renders.
Rule #3. Never create new components inside the render function of another component.
Rule #4. When using context, make sure that value
property is always memoised if itâs not a number, string or boolean.
That is it! Hope those rules will help to write more performant apps from the get-go and lead to happier customers who never had to experience slow products anymore.
Bonus: the useCallback
conundrum
I feel I need to resolve one mystery before I actually end this article: how can it be possible that useCallback
is useless for reducing re-renders, and why then React docs literally say that â[useCallback] is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary rendersâ? đ€Ż
The answer is in this phrase: âoptimized child components that rely on reference equalityâ.
There are 2 scenarios applicable here.
First: the component that received the callback is wrapped in React.memo
and has that callback as a dependency. Basically this:
const MemoisedItem = React.memo(Item);
const List = () => {
// this HAS TO be memoised, otherwise `React.memo` for the Item is useless
const onClick = () => {console.log('click!')};
return <MemoisedItem onClick={onClick} country="Austria" />
}
or this:
const MemoisedItem = React.memo(Item, (prev, next) => prev.onClick !== next.onClick);
const List = () => {
// this HAS TO be memoised, otherwise `React.memo` for the Item is useless
const onClick = () => {console.log('click!')};
return <MemoisedItem onClick={onClick} country="Austria" />
}
Second: if the component that received the callback has this callback as a dependency in hooks like useMemo
, useCallback
or useEffect
.
const Item = ({ onClick }) => {
useEffect(() => {
// some heavy calculation here
const data = ...
onClick(data);
// if onClick is not memoised, this will be triggered on every single render
}, [onClick])
return <div>something</div>
}
const List = () => {
// this HAS TO be memoised, otherwise `useEffect` in Item above
// will be triggered on every single re-render
const onClick = () => {console.log('click!')};
return <Item onClick={onClick} country="Austria" />
}
None of this can be generalised into a simple âdoâ or âdonât doâ, it can only be used for solving the exact performance problem of the exact component, and not before.
And now the article is finally done, thank you for reading it so far and hope you found it useful! Bleib gesund and see ya next time âđŒ
...
Originally published at https://www.developerway.com. The website has more articles like this đ
Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.