0
\$\begingroup\$

I have built a version of Conway's Game of Life in React. If you don't know what it is, the Game of Life is a simple algorithm that causes patterns and shapes to appear in a 2D array of cells.

While the app has functionality, I feel that it's performance could be significantly improved upon. I am trying to make it run as efficiently as possible.

Most of the logic is in the GridContainer component. I believe the createGrid() function which is called from componentDidMount() is causing the majority of the performance issues.

Index files which calls the App component:

var React = require('react');
var createReactClass = require('create-react-class');
var ReactDOM = require('react-dom');

// components
var App = require('./components/App.jsx');

ReactDOM.render(<App />, document.getElementById('app'));

var React = require('react');
var PropTypes = React.PropTypes;

App component which calls the main GridContainer component:

var React = require('react');
var createReactClass = require('create-react-class');

// components
var GridContainer = require('./GridContainer.jsx');

var App = createReactClass({
  render: function(){
    return(
      <GridContainer dim={51}/>
    );
  }
});

module.exports = App;

GridContainer component which has the state and logic:

var React = require('react');
var createReactClass = require('create-react-class');
var ReactDOM = require('react-dom');
var Row = require('react-bootstrap').Row;
var Col = require('react-bootstrap').Col;
var FontAwesome = require('react-fontawesome');

// components
var Cell = require('./Cell.jsx');
var Grid = require('./Grid.jsx');

var GridContainer = createReactClass({
  getInitialState: function() {
    return {
      matrix: [],
      generations: 0,
      neighborCells: [[0, 1], [1, 0], [-1, 0], [0, -1], [-1, -1], [1, 1], [-1, 1], [1, -1]]
    };
  },

  componentWillMount: function() {
    this.freshGrid();
  },

  freshGrid: function() {
    var matrix = [];
    var dim = this.props.dim;

    var Cell = function() {
      this.neighborsCount = 0;
      this.isAlive = false;
    }

    var counter = 0;
    for (var i = 0; i < dim; i++) {
      var row = [];
      for (var j = 0; j < dim; j++) {
        counter++;
        var cell = new Cell();
        row.push(cell);

        if (counter%2 == 0) {
          cell.isAlive = true;
        }
      }
      matrix.push(row);
    }

    this.setState({matrix: matrix});
  },

  createGrid: function() {
    this.interval = setInterval(function() {
      this.clearNeighborsCount(this.state.matrix);
      var newMatrix = JSON.parse(JSON.stringify(this.state.matrix));
      this.countNeighbours(newMatrix);
      this.updateCells(newMatrix);
      this.setState({matrix: newMatrix});
    }.bind(this), 100)
  },

  countNeighbours: function(newMatrix) {
    var dim = this.props.dim;

    for (var i = 0; i < dim; i++) {
      for (var j = 0; j < dim; j++) {
        this.countNeighboursCell(i, j, newMatrix);
      }
    }
  },

  countNeighboursCell: function(row, col, newMatrix) {
    var neighborCells = this.state.neighborCells;
    var cell = newMatrix[row][col];

    for (var i = 0; i < neighborCells.length; i++) {
      var neighborPos = neighborCells[i];
      var x = row + neighborPos[0];
      var y = col + neighborPos[1];

      if (this.isWithinGrid(x, y)) {
        var neighbor = newMatrix[x][y];
        if (neighbor.isAlive) {
          cell.neighborsCount++;
        }
      }
    }
  },

  isWithinGrid: function(row, col) {
    var dim = this.props.dim;

    if (row >= 0 && col >= 0 && row < dim && col < dim) {
      return true;
    }
    return false;
  },

  updateCells: function(newMatrix) {
    var dim = this.props.dim;

    for (var i = 0; i < dim; i++) {
      for (var j = 0; j < dim; j++) {
        var currentCell = newMatrix[i][j];

        if (currentCell.isAlive && (currentCell.neighborsCount == 2 || currentCell.neighborsCount == 3)) {
          currentCell.isAlive = true;
        } else if (!currentCell.isAlive && currentCell.neighborsCount == 3) {
          currentCell.isAlive = true;
        } else {
          currentCell.isAlive = false;
        }
      }
    }
  },

  clearNeighborsCount: function() {
    var dim = this.props.dim;

    for (var i = 0; i < dim; i++) {
      for (var j = 0; j < dim; j++) {
        this.state.matrix[i][j].neighborsCount = 0;
      }
    }
  },

  togglePause: function(e) {
    if (e.target.innerHTML === 'Pause') {
      clearInterval(this.interval);
      e.target.innerHTML = 'Play';
    } else {
      e.target.innerHTML = 'Pause';
      this.createGrid();
    }
  },

  reset: function() {
    this.freshGrid();
  },

  componentDidMount: function() {
    this.createGrid();
  },

  render: function() {
    var dim = this.props.dim;

    var cells = [];
    for (var i = 0; i < dim; i++) {
      var row = [];
      for (var j = 0; j < dim; j++) {
        row.push(<Cell dim={10} isAlive={this.state.matrix[i][j].isAlive} key={i*dim+j} row={i} col={j} />)
      }
      cells.push(row);
    };

    return (
      <Row>
        <Col xs={12} md={10}>
          <Grid cells={cells} dim={this.props.dim} />
        </Col>
    </Row>
    )
  }
});

module.exports = GridContainer;

Grid component which renders an array of Cell components:

var React = require('react');
var PropTypes = require('prop-types');

var Grid = function(props) {
  var gridStyle = {
    width: props.dim * 10,
    height: props.dim * 10,
    background: "#FAFAFA",
    margin: "0 auto",
    WebKitBoxShadow: "0 0 5px rgba(0, 0, 0, 1)",
    MozBoxShadow: "0 0 5px rgba(0, 0, 0, 1)",
    boxShadow: "0 0 5px rgba(0, 0, 0, 1)"
  };

  return (
    <div style={gridStyle}>
      {props.cells}
    </div>
  )
}

Grid.propTypes = {
  cells: PropTypes.array.isRequired
};

module.exports = Grid;

Individual cell component:

var React = require('react');
var PropTypes = require('prop-types');

var Cell = function(props) {
  var dim = props.dim;

  var cellStyle = {
        width: dim,
        height: dim,
        dislay: "inline-block",
        float: "left",
        border: "1px solid #000",
        background: props.isAlive ? "#FFF" : "#151515"
    }

  return (<div onClick={props.clicked} style={cellStyle}></div>)
};

Cell.propTypes = {
  dim: PropTypes.number.isRequired,
  clicked: PropTypes.func
}

module.exports = Cell;
\$\endgroup\$
1
\$\begingroup\$

I'm kind of in the same boat. I've tried a lot of things and am still looking for some performance updates.

The one hint I can give you is to replace the JSON.Stringify and JSON.parse with an array.map and array.slice as you can see in the first lines of my play method. in the App class. This will at least give a little boost when deep copying the array.

[EDIT] : A big sidenote, another reason this is slow beyond believe is because the browser has to do a repaint for every single frame you get. The bigger your grid gets, the more expensive this becomes. So if you really want to up the performance, get rid of the react and write it in a canvas I guess. Altho an even better option would be to move it away from the browser and run it in a native language.

import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Box extends Component {

    handleClick = () => {
        this.props.onClick(this.props.x, this.props.y);
    }
    render() {
        return <div className={this.props.className} onClick={this.handleClick}></div>
    }
}



const IntervalSlider = (props) => {
    return(
        <input type='range' min='100' max='2000' value={props.intervalTime} onChange={props.change}/>
    )
}

class App extends Component {
    constructor(props) {
        super(props);
        this.width = 96;
        this.height = 96;
        this.interval = null;
        this.intervalTime = 500;
        this.state = {
            fullGrid: this.createGrid()
        }
    }

    createGrid = () => {
        return new Array(this.height).fill(new Array(this.width).fill(false));
    }

    clickHandler = (x, y) => {
        let arrayToPass = JSON.parse(JSON.stringify(this.state.fullGrid));
        arrayToPass[x][y] = !arrayToPass[x][y];
        this.setState({
            fullGrid: arrayToPass
        });
    }

    autoPlay = () =>{
        clearInterval(this.interval);
        this.interval = setInterval(this.play,this.intervalTime);
    }

    stop = () => {
        clearInterval(this.interval);
    }

    clear = () => {
        clearInterval(this.interval);
        this.setState({
            fullGrid: this.createGrid()
        })
    }

    play = () => {

        let arrayToPass = this.state.fullGrid.map(function(arr) {
            return arr.slice();
        });
        let originalArray = arrayToPass.map(function(arr) {
            return arr.slice();
        });
        console.time('forloopz')
        for (let i = 0; i < arrayToPass.length; i++) {
            for (let j = 0; j < arrayToPass[i].length; j++) {
                let amountOfNeighbours = 0;
                let left = i-1 < 0 ? arrayToPass.length-1 : i-1;
                let right = i+1 >= arrayToPass.length? 0 : i+1;
                let up = j-1 < 0 ? arrayToPass[i].length-1 : j-1;
                let down = j+1 >= arrayToPass[i].length ? 0 : j+1;

                if (originalArray[right][j]){
                    amountOfNeighbours++;
                }
                if (originalArray[left][j]){
                    amountOfNeighbours++;
                }
                if (originalArray[right][down]){
                    amountOfNeighbours++;
                }
                if (originalArray[right][up]){
                    amountOfNeighbours++;
                }
                if (originalArray[left][down]){
                    amountOfNeighbours++;
                }
                if (originalArray[left][up]){
                    amountOfNeighbours++;
                }
                if (originalArray[i][down]){
                    amountOfNeighbours++;
                }
                if (originalArray[i][up]){
                    amountOfNeighbours++;
                }

                if (originalArray[i][j]){
                    if(amountOfNeighbours < 2 || amountOfNeighbours > 3){
                        arrayToPass[i][j] = false;
                    }else{
                        arrayToPass[i][j] = true;
                    }
                }else{
                    if(amountOfNeighbours === 3){
                        arrayToPass[i][j] = true;
                    }
                }
            }
        }
        console.timeEnd('forloopz');
        this.setState({
            fullGrid: arrayToPass,
        });
    }

    onIntervalChange = (ev) => {
        this.intervalTime = ev.target.value;
        this.autoPlay();
    }

    render() {

        return (

            <div>
                <button onClick={this.play}>Play</button>
                <button onClick={this.autoPlay}>AutoPlay</button>
                <button onClick={this.stop}>Stop AutoPlay</button>
                <IntervalSlider intervalTime={this.state.intervalTime} change={this.onIntervalChange}/>
                {this.state.fullGrid.map((element, ind) => {
                    return(
                        <div key={ind}>
                            {element.map((el, i) => {
                            let boxClass = el ? 'checked' : 'unchecked';
                            let boxID = i + '_' + ind;
                            return <Box id={boxID} key={boxID} className={boxClass} x={ind} y={i} onClick={this.clickHandler}/>
                        })}
                        </div>
                    )
                })}
            </div>

        )
    }
}

ReactDOM.render(<App />, document.getElementById('root'));
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.