Lateral
Lateral

Reputation: 3

How to optimize the re-rendering of large amounts of child components?

I'm a beginner when it comes to react.js and I'm building a component that contains a large number of changing items.

TLDR: I have a Parent component that contains many Child components (think > 1000) that change their state very quickly. However the state of the child components needs to be known in the parent component - therefore I lifted the state of all children to the parent component. Since all child components are rendered every time the state in the parent changes, the performance is pretty bad. A single changing pixel can take more than 200ms to update. Implementing shouldComponentUpdate on the Child component is still too slow. Do you have general advice how to handle such a case?

As a specific example of my issue I created an "graphics editor" example with a PixelGrid component consisting of 32 by 32 Pixel components:

JS Fiddle of example

When the onMouseDown or onMouseEnter event is called on the Pixel component, the event is passed up to the parent PixelGrid component through prop callbacks, and the corresponding state (PixelGrid.state.pixels[i].color) is changed. Keep in mind that the PixelGrid component is supposed to be able to access all pixel values for further functionality, so keeping state in Pixel itself is really not an option, I think. But this means, that the whole PixelGrid component needs to be re-rendered when a single pixel changes. This is obviously very slow. I implemented shouldComponentUpdate on the Pixel component to speed things up a little, but this is still not very fast, since every Pixel is tested for changes.

My first reaction was to manually change the pixel's inline CSS in the DOM through React refs and not keep the pixel state in this.state.pixels, but in this.pixels, so a state change doesn't cause re-rendering, but it seems like a bad to maintain the visual representation "manually".

So, how would you implement such a functionality with React?

Upvotes: 0

Views: 1983

Answers (2)

BLS
BLS

Reputation: 111

You're right to raise state to the parent so that it can control the data. Your Fiddle cannot be optimised because you are mutating state on line 82.

  setPixelColor(row, col, color){
  const {pixels} = this.state;
  pixels[row * this.props.cols + col].color = color; // mutates state
  this.setState({pixels: pixels});
}

When you run this.state.pixels[n].color = color, you're re-assigning a (nested) property on an item in the array in state. This is a form of mutation.

To avoid this, you can spread a copy of the state into a new variable and mutate that:

  setPixelColor(row, col, color){
  const newPixels = [...this.state.pixels]; // makes a fresh copy
  newPixels[row * this.props.cols + col].color = color; // mutate at your leisure
  this.setState({pixels: newPixels});
}

Implemented properly, it should not concern you whether "the whole PixelGrid component needs to be re-rendered when a single pixel changes." This is what needs to happen for your pixels to change colour. React's diffing algorithm is designed to update the minimum no. of elements as necessary. In your case, React will update only the style property on the relevant elements. (See React docs: Reconcilliation)

However, React cannot diff properly if it's not sure exactly what has changed, e.g if elements do not have unique keys, or if the element type changes (e.g. from <div> to <p>).

In your Fiddle, you map over the array, then calculate index for each one, and set that as they key. Even though you are not re-ordering the elements in your Fiddle example, you use let instead of const, and mention that this is not the whole code. If your real code employs index, or the current row or column, as a key, but the order changes, then React will unmount and remount every child. That would certainly affect your performance.

(You don't need to calculate the index, by the way, as it is available as the second param in .map, see MDN).

I'd recommend adding a unique id property to each initialised pixel object, which is set in the parent, and does not change. As you're not using data with uuids, perhaps their inital position:

            pixels.push({
                col: col, 
                row: row, 
                color: 'white',
                id: `row${row}-col${col}`; // only set once
            });

I've made a quick fiddle with the above changes in one to demonstrate that this works: https://jsfiddle.net/bethylogism/18ezpoqc/4/

Again, the React docs on Reconcilliation are very good, I highly recommend for anyone trying to optimise for whom memoisation is not working: https://reactjs.org/docs/reconciliation.html

Upvotes: 1

Bill Metcalf
Bill Metcalf

Reputation: 670

Use React.memo to prevent the child components from rendering when the parent renders but the child props don't change.

Example (random guess at what your Pixel component looks like):

const Pixel = React.memo(({x, y, color, ...rest}) => 
  <div style={{
      width: 1, 
      height: 1, 
      x, 
      y, 
      backgroundColor: color
    }}
    {...rest}
  />)

Now keep in mind if you are passing functions into Pixel they also need to be memoized. For instance, doing this is incorrect:

const Parent = () => {
  // the callback gets redefined whenever Parent rerenders, causing the React.memo to still update
  return <Pixel onClick={() => {}} />
}

instead you would need to do

const Parent = () => {
  const cb = useCallback(() => {}, []);
  return <Pixel onClick={cb} />
}

Upvotes: 1

Related Questions