2

A while ago, I developed a grid component to use in an Angular application. Each Grid has a number of rows, each of them being a Row component, and each Row has cells, that are also components, named Cell. Each cell can have a number of "objects" that basically are div tags with some styling and event listeners attached. Visually:

components layout

The reason of this design was the different functionalities that the components should provide to the user. For example, the grid component is responsible of getting the bulk data from the API, parsing it and build as many rows as needed. The row components can be disabled, tagged, moved above/below, create a new row, etc and they receive the data from the parent grid via @Input properties and build the cell components, also using @Input to pass their content. These cell components have any number of objects (these are not components) that can be dragged and dropped, deleted, cloned, etc.

This approach has been working fine until we had to manage big grids. In this case, the application becomes quite sluggish and it's almost unusable. I know that the reason is the enormous number of components being created, with their corresponding event listeners and all the stuff. To avoid this problem, it's mandatory to implement some sort of virtual scrolling, but I can't find how to do it, as the rows can have very different heights. So my question is, is there any mechanism that allows me to "sleep" the created componentes while they're not being displayed on screen? The Grid, Row and Cell components all use the OnPush change detection strategy, but it's being triggered constantly, when the user hovers a cell, an object, etc.

I'm implementing a pagination system, but there are cases when we'd need to display the sequence of all the rows and I don't know if it'll be possible with the current approach.

Any suggestion? Thanks in advance!

Fel
  • 4,428
  • 9
  • 43
  • 94
  • 1
    If change detection is triggered constantly you have your first problem there. Do you modify base object on every action? Did you take a look at https://material.angular.io/cdk/scrolling/overview? Assuming you are using *ngFor internally, do you use trackBy? – Bojan Kogoj Feb 19 '21 at 14:38
  • Regarding the change detection triggering, I've reduced it a lot by following the instructions on this post: https://stackoverflow.com/questions/50825931/disable-zone-js-mousemove-for-changedetect/50872389. The base object is modified only on CRUD operations, f.i. when a new rule is added, and I use `trackBy` in the `*ngFor`. I checked the CDK library, but couldn't find anything that suited my needs. I'm still doing tests, I'll post any useful discovering I make. – Fel Feb 19 '21 at 15:00
  • I don't know if it can work in your case, but you have lots of cells and when a change detection is triggered in a parent component, all children are triggered too. So, what if you detach your cell components from the change detection tree ? this way, you will optimize your grid list rendering. But you have to be careful when you detach a component from the change detection tree, if you're exepecting your child to do "something" because of a change made in the parent component, you need to manage it manually. – OLO Feb 19 '21 at 15:46
  • The cell components use the `OnPush` change detection strategy, and I thought it was equivalent to detach the component. I'll do some tests now to see if it improves the performance, but I'm thinking about refactoring the grid component to avoid creating so many sub-components. I'll post the results in a bit. – Fel Feb 22 '21 at 07:20

2 Answers2

1

I was stuck on the same problem.. a grid like view with a component for each cell. When the number of cells grew, the app essentially crashed.


I managed to solve it like this:

Add a property to the data for each cell, such as readOnly, and set it to true by default:

public items = [
    ...
    {
        _id: 'some-id',
        value: 'some-value',
        readOnly: true
        ...
    },
    ...
];

Then, in your template, do it like this:

<td (mouseover)="item.readOnly=false">

    <ng-container *ngIf="item.readOnly">
        {{ item.value }}
    </ng-container>
    
    <ng-container *ngIf="!item.readOnly">
        <ng-container *ngTemplateOutlet="edit; context: { item: item }">
        </ng-container>
    </ng-container>

</td>

...

<!-- Place template at the root/bottom of the HTML -->
<ng-template #edit let-item="item">
    <app-component [content]="item"></app-component>
</ng-template>

The way this works, is by completely skipping the loading of the component by default, and display a flat value as text. As soon as you hover the cell, it lazy loads a template with the component. Every time the template is called, it returns a new instance of the template, so there can be many different cells active at the same time too.

All the others solutions I found were making use of templateRefs, QueryLists, ComponentLoaders, etc, which adds a whole (unnecessary) layer of complexity. That's not needed at all.


Watch out with deactivating cells with (mouseout) though, since hovering the newly loaded component may count as a mouseout, thus switching back to read-only.

Jeffrey Roosendaal
  • 6,872
  • 8
  • 37
  • 55
  • 1
    Hi! Thanks for your suggestion, it's a technique that I've used (with a slightly different logic) in some components. In this case, due to the needed features, what I've done is a sort of virtualization. I load all the rows, but I only render completely (I mean, with event handlers) the ones that are shown in the viewport. All the rest are "dumb", only a placeholder to take up the needed space. When the user scrolls is when I "activate" the visible rows. This way, the application can manage grids with thousands of rows with a perfect performance. Cheers! – Fel May 06 '21 at 15:03
0

After some tests, I've come to the conclusion that I have to refactor the code to integrate the rows and cells into the grid without instantiating them as components. I think that this change will improve the performance a lot, as well as facilitating other tasks as the pagination and the value passing between elements.

It will be a big refactoring, but I believe it's the best solution for a case like this, where the UI has to deal with thousands of components.

Cheers,

Fel
  • 4,428
  • 9
  • 43
  • 94