Search code examples
arraysangularcss-gridconways-game-of-lifeangular-changedetection

How to get Angular to only redraw elements of a CSS grid when their corresponding array element has changed


I've implemented Conway's Game of Life in Angular using CSS grid. I've noticed that even with a relatively small grid (50 squares x 50 squares) there is a discernible visual lag between clicking on the button for the next tick and the update of the grid.

I suspected that Angular was redrawing all of the squares every tick, even though only a small percentage of them actually change. I added some fading animation that confirmed my suspicion. Actually, not entirely. Sometimes, certain cells aren't redrawn but it almost seems random. Most cells are inactive (white) and so do not need to be redrawn. Yet most of them are, but not all. Sometimes a few rows won't be redrawn and the rest will.

I've tried adding a trackBy function to track by index. That didn't seem to help.

Here is a link to the project on stackblitz: https://stackblitz.com/edit/angular-conway?file=src/app/app.component.ts

Here is the code:

<div
  style="display: grid; grid-template-columns: repeat(50, 15px); grid-template-rows: repeat(50, 15px)"
>
  <div
    *ngFor="let alive of grid; index as i"
    class="my-animation"
    [ngStyle]="{'border': 'solid black 1px', 'background-color':  alive ? 'black' : 'white'}"
  ></div>
</div>

<button (click)="nextTick()">Next</button>
import { ChangeDetectionStrategy, Component, OnInit } from "@angular/core";
import { getNeighbors } from "../../get-neighbors";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
  grid: boolean[] = [];

  ngOnInit(): void {
    this._randomStart(0.1);
  }

  private _randomStart(probabilityOfAlive: number): void {
    for (let i = 0; i < 2500; i++) {
      this.grid[i] = Math.random() <= probabilityOfAlive;
    }
  }

  nextTick(): void {
    this._updateGrid();
  }

  private _updateGrid(): void {
    const newGrid = [];

    for (let i = 0; i < 2500; i++) {
      const isAlive = this.grid[i];
      const liveNeighborsCount = this._getLiveNeighborsCount(i);
      if (isAlive && (liveNeighborsCount === 2 || liveNeighborsCount === 3))
        newGrid[i] = true;
      else if (!isAlive && liveNeighborsCount === 3) newGrid[i] = true;
      else newGrid[i] = false;
    }

    this.grid = newGrid;
  }

  private _getLiveNeighborsCount(cellIndex: number): number {
    return getNeighbors({ index: cellIndex, width: 50, heigth: 50 }).reduce(
      (sumOfAlive, indexOfNeighbor) => {
        return this.grid[indexOfNeighbor] ? sumOfAlive + 1 : sumOfAlive;
      },
      0
    );
  }
}

Solution

  • In the end, the only thing wrong with my original implementation was the trackBy function. I had inverted the params order and thus was tracking by value instead of by index. Once I implemented the function properly everything worked as expected without having to make any other changes.

    trackByIndex(index: number): number {
      return index;
    }