Dirk J. Faber
Dirk J. Faber

Reputation: 4701

React is slow when rendering many elements

Component Cell has the state alive which can be true or false.

If alive is true then Cell is rendered as a div with a class alive (think of Conway's Game of Life). The state alive updates every second:

function Cell(props) {
  const [alive, setAlive] = useState(
    Math.random() < props.aliveProbability / 100
  );

  useEffect(() => {
    const interval = setInterval(
      () => setAlive(Math.random() < props.aliveProbability / 100),
      1000
    );
    return () => clearInterval(interval);
  }, []);

  return (
    <div className={"cell " + (alive ? "alive" : "")}>{props.children}</div>
  );
}

With a single cell this works fine. But when adding multiple cell components to a grid, the rendering slows down and is happening in sequentially instead of simultaneously. The more cells, the slower it becomes. How can this be resolved?

A working demo can be seen here.

Upvotes: 5

Views: 13544

Answers (2)

Emile Bergeron
Emile Bergeron

Reputation: 17430

The reason it is slow is because each cell, at a slightly different time, triggers a new render on their own since each setInterval callback gets called individually.

Instead of having an interval per cell, lift up the state to the parent component, do one single interval and update of the whole dataset, then render down the actual tree with the resulting data once. React will take care of optimizing the DOM changes!

Here's a simplified example using a single array of cell.

// Simple "dumb" cell component
function Cell({ alive, children }) {
  return <div className={"cell " + (alive ? "alive" : "")}>{children}</div>;
}

// The app manages the update cycle
function App() {
  // Initial dataset
  const [cells, setCells] = useState([
    { alive: true, aliveProbability: 0.9 },
    { alive: true, aliveProbability: 0.9 },
    { alive: true, aliveProbability: 0.9 },
  ]);

  useEffect(() => {
    const interval = setInterval(
      () =>
        setCells((cells) =>
          // update the whole dataset once every interval
          cells.map(({ aliveProbability }) => ({
            alive: Math.random() < aliveProbability / 100,
            aliveProbability,
          }))
        ),
      1000
    );
    return () => clearInterval(interval);
  }, []);

  // Render the whole dataset once.
  return <div>{cells.map(cellProps => <Cell {...cellProps} />)}</div>;
}

Now, instead of thousands of individual renders, there's only a single update and render every second.

Like in a game engine, there's now a single game loop, not thousands of game loops!


If game development with React is something you'd like to dive into, there are already great examples:

Upvotes: 7

jered
jered

Reputation: 11591

You should be managing the "alive" state of each cell at the app level, not letting each individual cell track its own "alive" state and updating it independently. Doing so will cause exponentially more state updates the larger your grid is - 100 updates on a 10x10 grid, 1200 updates on a 30x40, and 10000 updates on a 100x100.

Furthermore, because each of those state updates is the result of a separate useEffect call, they are not guaranteed to happen simultaneously. React will batch them together as efficiently as it can, and will probably do them in order, but they will be split into asynchronous tasks that can be done in between browser paint frames, hence the "slow" row-by-row rendering.

Instead, you should update all of your cells in a single pass, tracking their state at the app level and passing that state to children.

https://codesandbox.io/s/upbeat-waterfall-g4ndh?file=/src/App.js

"Lifting state up" is a critical concept in React, so much so it has its own section in the official React documentation, so read it carefully and understand it well!

Upvotes: 1

Related Questions