- React and React Native
- Adam Boduch
- 1113字
- 2021-07-09 19:28:56
Optimize rendering efficiency
The next lifecycle method you're going to learn about is used to implement heuristics that improve component rendering performance. You'll see that if the state of a component hasn't changed, then there's no need to render. Then, you'll implement a component that uses specific metadata from the API to determine whether or not the component needs to be re-rendered.
To render or not to render
The shouldComponentUpdate()
lifecycle method is used to determine whether or not the component will render itself when asked to. For example, if this method were implemented, and returned false, the entire lifecycle of the component is short-circuited, and no render happens. This can be an important check to have in place if the component is rendering a lot of data and is re-rendered frequently. The trick is knowing whether or not the component state has changed.
This is the beauty of immutable data—we can easily check if it has changed. This is especially true if we're using a library such as Immutable.js
to control the state of the component. Let's take a look at a simple list component:
import React, { Component } from 'react'; import { fromJS } from 'immutable'; export default class MyList extends Component { state = { data: fromJS({ items: new Array(5000) .fill(null) .map((v, i) => i), }), }; // Getter for "Immutable.js" state data... get data() { return this.state.data; } // Setter for "Immutable.js" state data... set data(data) { this.setState({ data }); } // If this method returns false, the component // will not render. Since we're using an Immutable.js // data structure, we simply need to check for equality. // If "state.data" is the same, then there's no need to // render because nothing has changed since the last // render. shouldComponentUpdate(props, state) { return this.data !== state.data; } // Renders the complete list of items, even if it's huge. render() { const items = this.data.get('items'); return ( <ul> {items.map(i => ( <li key={i}>{i}</li> ))} </ul> ); } }
The items
state is initialized to an Immutable.js
List
with 5000
items in it. This is a fairly large collection, so we don't want the virtual DOM inside React to constantly compute differences. The virtual DOM is efficient at what it does, but not nearly as efficient as code that can perform a simple should or shouldn't render check. The shouldComponentRender()
method we've implemented here does exactly that. It compares the new state with the current state; if they're the same object, we completely sidestep the virtual DOM.
Now, let's put this component to work, and see what kind of efficiency gains we get:
import React from 'react'; import { render as renderJSX } from 'react-dom'; import MyList from './MyList'; // Renders the "<MyList>" component. Then, it sets // the state of the component by changing the value // of the first "items" element. However, the value // didn't actually change, so the same Immutable.js // structure is reused. This means that // "shouldComponentUpdate()" will return false. function render() { const myList = renderJSX( (<MyList />), document.getElementById('app') ); // Not actually changing the value of the first // "items" element. So, Immutable.js recognizes // that nothing changed, and instead of // returning a new object, it returns the same // "myList.data" reference. myList.data = myList.data .setIn(['items', 0], 0); } // Instead of performing 500,000 DOM operations, // "shouldComponentUpdate()" turns this into // 5000 DOM operations. for (let i = 0; i < 100; i++) { render(); }
As you can see, we're just rendering <MyList>
, over and over, in a loop. Each iteration has 5,000 list items to render. Since the state doesn't change, the call to shouldComponentUpdate()
returns false
on every one of these iterations. This is important for performance reasons, because there are a lot of them. Obviously, we're not going to have code that re-renders a component in a tight loop, in a real application. This code is meant to stress the rendering capabilities of React. If you were to comment out the shouldComponentUpdate()
method, you'd see what I mean.
Note
You may notice that we're actually changing state inside our render()
function using setIn()
on the Immutable.js
map. This should result in a state change, right? This will actually return the same Immutable.js
instance for the simple reason that the value we've set is the same as the current value: 0
. When no change happens, Immutable.js
methods return the same object, since it didn't mutate. Cool!
Using metadata to optimize rendering
In this section, we'll look at using metadata that's part of the API response to determine whether or not the component should re-render itself. Here's a simple user details component:
import React, { Component } from 'react'; export default class MyUser extends Component { state = { modified: new Date(), first: 'First', last: 'Last', }; // The "modified" property is used to determine // whether or not the component should render. shouldComponentUpdate(props, state) { return +state.modified > +this.state.modified; } render() { const { modified, first, last, } = this.state; return ( <section> <p>{modified.toLocaleString()}</p> <p>{first}</p> <p>{last}</p> </section> ); } }
If you take a look at the shouldComponentUpdate()
method, you can see that it's comparing the new modified
state to the old modified
state. This code makes the assumption that the modified
value is a date that reflects when the data returned from the API was actually modified. The main downside to this approach is that the shouldComponentUpdate()
method is now tightly coupled with the API data. The advantage is that we get a performance boost in the same way that we would with immutable data.
Here's how this heuristic looks in action:
import React from 'react'; import { render } from 'react-dom'; import MyUser from './MyUser'; // Performs the initial rendering of "<MyUser>". const myUser = render( (<MyUser />), document.getElementById('app') ); // Sets the state, with a new "modified" value. // Since the modified state has changed, the // component will re-render. myUser.setState({ modified: new Date(), first: 'First1', last: 'Last1', }); // The "first" and "last" states have changed, // but the "modified" state has not. This means // that the "First2" and "Last2" values will // not be rendered. myUser.setState({ first: 'First2', last: 'Last2', });
As you can see, the component is now entirely dependent on the modified
state. If it's not greater than the previous modified value, no render happens.
Here's what the component looks like after it's been rendered twice:
Note
In this example, I didn't use immutable state data. Throughout this book, I'll use plain JavaScript objects as state for simple examples. Immutable.js
is a great tool for this job, so I'll be using it a lot. At the same time, I want to make it clear that Immutable.js
doesn't need to be used in every situation.