- Performance Optimisations for React Applications
- The Nuggets translation Project
- Translator: woota
- Proofreader: Malcolm U, Zheaoli
The point outline
The main performance issues with React applications are redundant processing and DOM alignment of components. To avoid these performance traps, you should return false in shouldComponentUpdate whenever possible.
In short, it comes down to the following two points:
- To speed up shouldComponentUpdateThe check
- To simplify the shouldComponentUpdateThe check
Disclaimer!
The example in this article is written with React + Redux. If you are using another data flow library, the principles are the same but the implementation will be different.
I didn’t use the Immutability library in this article, just some plain ES6 and a bit of ES7. Some things are a little easier with immutable databases, but I’m not going to cover that here.
What are the main performance issues with React applications?
- Redundant operations in components that do not update the DOM
- The DOM compares leaf nodes that do not need to be updated
- While DOM alignment is great and speeds up React, the computational cost is significant
How does React default render behavior?
Let’s look at how React renders components.
Initialize render
When initializing the render, we need to render the entire application (green = rendered nodes)
Every node is rendered — it’s awesome! Now our application represents our initial state.
Put forward the change
We want to update some of the data. These changes are associated with only one leaf node
Ideal update
We only want to render these nodes along the critical path to the leaf node
The default behavior
If you don’t tell React not to do this, it will (orange = wasteful rendering)
Oh, no! All of our nodes have been re-rendered.
Every component of React has a shouldComponentUpdate(nextProps, nextState) function. Its job is to return true when the component needs to be updated and false when the component does not need to be updated. Returning false causes the component’s render function not to be called. React always returns true in shouldComponentUpdate by default, even if you don’t explicitly define a shouldComponentUpdate function.
// Default behavior
shouldComponentUpdate(nextProps, nextState) {
return true;
}Copy the code
This means that by default, every time you update your props at the top level, every component of the entire application is rendered. This is a major performance issue.
How do we get the desired updates?
Return false in shouldComponentUpdate if possible.
In short:
- To speed up shouldComponentUpdateThe check
- To simplify the shouldComponentUpdateThe check
Accelerate shouldComponentUpdate check
Ideally we don’t want to do deep checking in shouldComponentUpdate as this is very expensive, especially if you have large scale and large data structures.
class Item extends React.component {
shouldComponentUpdate(nextProps) {
// It's expensive
return isDeepEqual(this.props, nextProps);
}
// ...
}Copy the code
An alternative is to change the object’s reference whenever its value changes.
constnewValue = { ... oldValue// Make the changes you want here
};
// Quick check -- just check the references
newValue === oldValue; // false
// You can also use object. assign if you wish
const newValue2 = Object.assign({}, oldValue);
newValue2 === oldValue; // falseCopy the code
Use this technique in the Redux Reducer:
In this Reducer we will change the description of an item
export default (state, action) {
if(action.type === 'ITEM_DESCRIPTION_UPDATE') {
const { itemId, description } = action;
const items = state.items.map(item => {
// Action has nothing to do with this item -- we can return this item without modification
if(item.id ! == itemId) {return item;
}
// We want to change this item
// This preserves the original item value, but
// A new object with updated description is returned
return {
...item,
description
};
});
return {
...state,
items
};
}
return state;
}Copy the code
If you do this, you just need to check for references in the shouldComponentUpdate function
// Super fast -- all you do is check references!
shouldComponentUpdate(nextProps) {
return isObjectEqual(this.props, nextProps);
}Copy the code
An example implementation of isObjectEqual
const isObjectEqual = (obj1, obj2) => {
if(! isObject(obj1) || ! isObject(obj2)) {return false;
}
// Whether references are the same
if(obj1 === obj2) {
return true;
}
// Do they contain the same key names?
const item1Keys = Object.keys(obj1).sort();
const item2Keys = Object.keys(obj2).sort();
if(! isArrayEqual(item1Keys, item2Keys)) {return false;
}
// Does every object corresponding to the attribute have the same reference?
return item2Keys.every(key => {
const value = obj1[key];
const nextValue = obj2[key];
if(value === nextValue) {
return true;
}
// Check the depth of one level
return Array.isArray(value) &&
Array.isArray(nextValue) &&
isArrayEqual(value, nextValue);
});
};
const isArrayEqual = (array1 = [], array2 = []) => {
if(array1 === array2) {
return true;
}
// Check a level of depth
return array1.length === array2.length &&
array1.every((item, index) => item === array2[index]);
};Copy the code
Simplify shouldComponentUpdate check
Let’s start with a complex shouldComponentUpdate example
// Focus on separate data structures (standardized data)
const state = {
items: [
{
id: 5,
description: 'some really cool item'}]// Represents the object that the user interacts with the system
interaction: {
selectedId: 5}};Copy the code
If you organize your data this way, it makes checking in shouldComponentUpdate difficult
import React, { Component, PropTypes } from 'react'
class List extends Component {
propTypes = {
items: PropTypes.array.isRequired,
iteraction: PropTypes.object.isRequired
}
shouldComponentUpdate (nextProps) {
// Does the element in items change?
if(! isArrayEqual(this.props.items, nextProps.items)) {
return true;
}
// Things can get scary from here
// If interaction does not change, it can return false (good!).
if(isObjectEqual(this.props.interaction, nextProps.interaction)) {
return false;
}
// If the code runs here, we know:
// 1. Items do not change
// How much interaction do you have
// We need to see if the interaction changes are relevant to us
const wasItemSelected = this.props.items.any(item => {
return item.id === this.props.interaction.selectedId
})
const isItemSelected = nextProps.items.any(item => {
return item.id === nextProps.interaction.selectedId
})
// Return true if it has changed
// Return false if no change has occurred
returnwasItemSelected ! == isItemSelected; } render() {<div>
{this.props.items.map(item => {
const isSelected = this.props.interaction.selectedId === item.id;
return (<Item item={item} isSelected={isSelected} />);
})}
</div>}}Copy the code
Question 1:shouldComponentUpdatebulky
You can see that a shouldComponentUpdate corresponding to a very simple data is huge and complex. This is because it needs to know the structure of the data and how they relate to each other. The shouldComponentUpdate function only grows in complexity and size with your data structure. This can easily lead to two mistakes:
- Return false when it should not (application displays error status)
- Return true when it should not (causing performance problems)
Why make things so complicated? You just want to make these tests so easy that you don’t have to think about them at all.
Problem 2: Strong coupling between parent and child levels
In general, applications promote loose coupling (the less one component knows about the other, the better). The parent component should try to avoid knowing how its children work. This allows you to change the behavior of the child components without letting the parent know about the changes (assuming the PropsTypes remain unchanged). It also allows child components to operate independently without having their parent tightly control their behavior.
Solutions:Flatten your data
By flattening (merging) your data structures, you can reuse very simple reference checks to see if anything has changed.
const state = {
items: [
{
id: 5,
description: 'some really cool item'.// Interaction now exists inside item
interaction: {
isSelected: true}}}};Copy the code
Organizing your data this way makes it easy to check in shouldComponentUpdate
import React, {Component, PropTypes} from 'react'
class List extends Component {
propTypes = {
items: PropTypes.array.isRequired
}
shouldComponentUpdate(nextProps) {
// So easy, Mom no longer need to worry about my update check
return isObjectEqual(this.props, nextProps);
}
render() {
<div>
{this.props.items.map(item => {
return (
<Item item={item}
isSelected={item.interaction.isSelected} />)})}</div>}}Copy the code
If you want to update interaction you change the entire object reference
// redux reducer
export default (state, action) => {
if(action.type === 'ITEM_SELECT') {
const { itemId } = action;
const items = state.items.map(item => {
if(item.id ! == itemId) {return item;
}
// Change the reference to the entire object
return {
...item,
interaction: {
isSelected: true}}})return {
...state,
items
};
}
return state;
};Copy the code
Error: Reference checking with dynamic props
An example of creating dynamic props
class Foo extends React.Component {
render() {
const {items} = this.props;
// This object has a new reference every time
const newData = { hello: 'world' };
return <Item name={name} data={newData} />}} class Item extends React.Component {// Check always returns true even if two objects have the same value, ShouldComponentUpdate (nextProps) {return isObjectEqual(this. Props, nextProps); }}Copy the code
Usually we don’t create a new props in the component to pass it down. However, this is more common in loops
class List exntends React.Component {
render() {
const {items} = this.props;
<div>Const newData = {hello: 'world', isFirst: index === 0}; {items.map((item, index) => {// This object gets a new reference every time return<Item name={name} data={newData} />
})}
</div>}}Copy the code
This is common when creating functions
import myActionCreator from './my-action-creator';
class List extends React.Component {
render() {
const {items, dispatch} = this.props;
<div>{items.map(item => {// The reference to this function becomes const callback = () => {dispatch(myActionCreator(item)); } return<Item name={name} onUpdate={callback} />
})}
</div>}}Copy the code
Problem-solving strategies
- Avoid creating dynamic props in components
Improve your data model so that you can pass down props directly
- Pass the dynamic props to a type that satisfies congruence (===)
eg:
– boolean
– number
– string
const bool1 = true;
const bool2 = true;
bool1 === bool2; // true
const string1 = 'hello';
const string2 = 'hello';
string1 === string2; // trueCopy the code
If you really need to pass a dynamic object, pass it as a string and deconstruct it at the child level
render() {
const {items} = this.props;
<div>Const bad = {id: item.id, type: item.type}; / / the same value can meet the strict congruent '= = =' const good = ` ${item. Id} : : ${item. Type} `; return<Item identifier={good} />
})}
</div>
}Copy the code
Special case: function
- Avoid transfer functions if you can. Instead, dispatch the child component freely. This has the added benefit of moving business logic out of the component.
- Ignore function checking in shouldComponetUpdate. That’s not ideal, because we don’t know if the value of the function has changed.
- Create an immutable binding for data -> function. You can save them to the state in componentWillReceiveProps functions. This way you don’t get a new reference every time you render. This method is extremely cumbersome because you need to maintain and update a list of functions.
- Create an intermediate component with the correct this binding. This is also not ideal because you introduce a layer of redundancy into the hierarchy.
- Any other method you can think of that avoids creating a new function every time render is called.
An example of scenario 4
// Add another layer of 'ListItem'
<List>
<ListItem>// You can create the correct this binding here<Item />
</ListItem>
</List>Class ListItem extends React.Component {// Always get the correct this binding because it is bound to the instance // Thanks to ES7! const callback = () => { dispatch(doSomething()); } render() { return<Item callback={this.callback} item={this.props.item} />}}Copy the code
tool
All of the rules and techniques listed above were discovered using performance measurement tools. Using tools can help you identify specific performance issues in your application.
console.time
This one is pretty simple:
- Start a timer
- Do something
- Stop timer
A good practice is to use Redux middleware:
export default store => next => action => {
console.time(action.type)
// 'next' is a function that takes 'action' and sends it to 'reducers' for processing
// This will result in the first rendering you should have
const result = next(action);
// How long did the rendering take?
console.timeEnd(action.type);
return result;
};Copy the code
Use this method to track every action you apply and the time it takes to render it. You can quickly know which actions take the longest to render, so you can start from there when you solve performance problems. Getting a time value can also help you determine if the performance tuning you’ve done is working.
React.perf
This tool works the same as console.time, but uses the React performance tool:
- Perf.start()
- do stuff
- Perf.stop()
Examples of Redux middleware:
import Perf from 'react-addons-perf';
export default store => next => action => {
const key = `performance:${action.type}`;
Perf.start();
// Get the new state rendering application
const result = next(action);
Perf.stop();
console.group(key);
console.info('wasted');
Perf.printWasted();
// You can print any Perf measurements you are interested in here
console.groupEnd(key);
return result;
};Copy the code
Similar to the console.time method, it lets you see the performance metrics for each of your actions. More information about React performance addon can be found here
Browser tools
The CPU Analyzer flame chart also comes in handy when looking for performance problems with your application.
When doing performance analysis, the flame chart shows the state of your code’s Javascript stack every millisecond. As you record, you know exactly which function was executed at any point in time, how long it was executed, and who called it. – Mozilla
Firefox: Click to view
Chrome: Click to see
Thanks for reading, and good luck building a high-performance React app!