angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.32k stars 6.73k forks source link

mat-select seems to have serious performance issue #7623

Open naveedahmed1 opened 6 years ago

naveedahmed1 commented 6 years ago

Bug, feature request, or proposal:

With latest release, mat-form-field support was added for mat-select. But it introduced performance issues when we have multiple select lists (5-8).

When we use mat-form-field with input or text area it doesn't seem to cause any performance issue but it seem that its implementation for mat-select needs a review. On lengthy forms we are seeing a delay of up to 3 seconds, which are instant without mat-form-field on mat-select.

What is the expected behavior?

It should smooth and fast.

What is the current behavior?

Its very slow.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Angular 4.4.4 Material 2.0.0-beta.12 Chrome

crisbeto commented 6 years ago

It’s not really doing a lot differently from the previous approach. Can you post an example of a form that you consider slow?

naveedahmed1 commented 6 years ago

Have you noticed performance issues when using mat-form-field with mat-select?

crisbeto commented 6 years ago

I haven't when trying against our various test apps, but your use case might be different.

naveedahmed1 commented 6 years ago

In my case I have dynamic forms, but till last beta the form in my app were very smooth, but after this update forms with select list are loading slow. Can you please try it with some form and around 5-6 select list?. For clear idea add a button to toggle visibility of form.

crisbeto commented 6 years ago

Can you post an example of a slow form? You can use one of these as a base: https://goo.gl/DlHd6U or https://goo.gl/wwnhMV.

naveedahmed1 commented 6 years ago

Can you please take a look at https://stackblitz.com/edit/angular-material2-issue-kdt9y4?file=app%2Fapp.component.ts

crisbeto commented 6 years ago

I see. The reason is because all of those 5x100 options are rendered on init, even though they're not displayed, however this is how the select has always worked and definitely hasn't changed since the last beta. We're currently discussing better ways to handle larger lists of options at https://github.com/angular/material2/issues/5113.

naveedahmed1 commented 6 years ago

If I remove <mat-form-field>, its very smooth, so I think the problem is with <mat-form-field>.

crisbeto commented 6 years ago

I'm not sure I see a difference, but maybe we're talking about different things. I was referring to the delay after toggling the form.

naveedahmed1 commented 6 years ago

I am also referring to the rendering of form after we click toggle button i.e. delay between the button click and the moment when everything is rendered on screen. And what I am saying is delay is noticeable when we surround mat-select with mat-form-field. I have also gone through https://github.com/angular/material2/issues/5113 may be its due to this issue but in my case its noticeable after this change .

naveedahmed1 commented 6 years ago

One more thing, if you open few select lists from form, then toggle form to hide it and then open again, it will load even slower.

jagomf commented 6 years ago

Also, conditionally hiding options throws an error.

Consider this case:

<mat-form-field>
  <mat-select placeholder="Country" [(ngModel)]="value" (blur)="onBlur()" class="select-country-main">
    <ng-template ngFor let-country [ngForOf]="countries" [ngForTrackBy]="trackByCountryId">
      <mat-option *ngIf="!isExcluded(country)" [value]="country">
        <country-flag [country]="country"></country-flag>
      </mat-option>
    </ng-template>
  </mat-select>
</mat-form-field>

You get an error:

Error: mat-form-field must contain a MatFormFieldControl.

Before this would work perfectly.

mmalerba commented 6 years ago

@jagomf Are you sure you're using the latest version of material? Your example seems to work fine: https://stackblitz.com/edit/angular-material2-issue-dtceox?file=app/app.component.ts

Also please file a separate issue rather than tacking on to unrelated issues

jagomf commented 6 years ago

You're right @mmalerba, updating everything to 5.0 seems to work fine. However, this makes for a number of prior versions in which it wouldn't work fine (in my case it was Angular 4.4.6 with material 2.0 beta 11).

imchaitanya9 commented 6 years ago

Still facing issues while having multiple mat-select with "multiple" property.

@angular/material: 5.0.2 Angular: 5.1.1 @angular/cdk: 5.0.2

ronaldo127 commented 6 years ago

I'm facing a similar issue, but in my case I generate 48 options per mat-select and I generate 7 mat-select. My code is similar to this

These are my angular dependencies on my package.json "@angular/animations": "^5.0.0", "@angular/cdk": "^5.0.0", "@angular/common": "^5.0.0", "@angular/compiler": "^5.0.0", "@angular/core": "^5.0.0", "@angular/forms": "^5.0.0", "@angular/http": "^5.0.0", "@angular/material": "^5.0.0", "@angular/platform-browser": "^5.0.0", "@angular/platform-browser-dynamic": "^5.0.0", "@angular/router": "^5.0.0",

zachberger commented 6 years ago

I was able to resolve this by ensuring my mat-form-field was inside a form. Outside a form performance was abysmal.

chaimmw commented 5 years ago

I was using mat select with @ngrx but my select had only 5 options, and it took about 5 seconds for options to appear.

when I used a <select> there was no problem.

nexen505 commented 5 years ago

I'll admit that issue still exists in fresh version of Angular only with 10 mat-selects 4 mat-options each for Chrome 72.0.3626.96. Browser stucked API-requests and never rendered.

Versions: "@angular/animations": "^7.2.2", "@angular/cdk": "7.0.1", "@angular/common": "^7.2.2", "@angular/compiler": "^7.2.2", "@angular/core": "^7.2.2", "@angular/forms": "^7.2.2", "@angular/http": "^7.2.2", "@angular/material": "7.0.1", "@angular/material-moment-adapter": "^7.2.2", "@angular/platform-browser": "^7.2.2", "@angular/platform-browser-dynamic": "^7.2.2", "@angular/router": "^7.2.2",

w0wka91 commented 5 years ago

Is there any progress on this?

garg10may commented 4 years ago

This should be resolved at earliest, select are essential part of form. In my case I have multi select with 1000+ options and is loads absymally slow.

naveedahmed1 commented 4 years ago

Has the performance downgraded further? Countries list with ~250 items now takes 2-3 seconds to open, after I tap/click .

jeohes commented 3 years ago

I was also having this issue with material select. In our form we have around 12 dropdown boxes in which we had around 10-600 items being loaded per dropdown box. Angular Material select fields were being used for all of these. Our entire form became very slow with a lot of input lag on every input box. The entire form just seemed to choke up. We switched to just using regular native select/option dropdowns and everything was instantly snappy again. Hope this helps someone!

This is what our original mat select looked like:

<mat-form-field style="width: 100px;" appearance="fill">
                    <mat-label>Suffix</mat-label>
                    <mat-select [(ngModel)]="SUFFIX" name="SUFFIX">
                        <mat-option 
                          *ngFor="let suffixItem of suffixList" 
                          value="{{ suffixItem.SUFF_CODE }}">
                          {{ suffixItem.SUFF_CODE }}
                        </mat-option>
                    </mat-select>
</mat-form-field>

We then switched it to be just the native html:

<label>Suffix</label>
        <select formControlName="SUFFIX" name="SUFFIX">
            <option 
                *ngFor="let suffixItem of suffixList" 
                value="{{ suffixItem.SUFF_CODE }}">
                {{ suffixItem.SUFF_CODE }}
            </option>
        </select>
mleibman commented 2 years ago

As @jelbourn mentioned before, fixing this in Angular Material would require a breaking API change, but for really bad cases with lots of <mat-option>s, I've been experimenting with a custom pipe that will let the currently-selected option(s) through until the select is either opened or the page has been idle long enough to load the rest.

import {
  Pipe,
  PipeTransform,
  OnDestroy,
  ChangeDetectorRef,
} from '@angular/core';
import { MatSelect } from '@angular/material/select';
import { Subscription } from 'rxjs';

/**
 * Initializes only the selected `<mat-option>`s to remove the up-front cost of
 * initializing and rendering off-DOM all of the select options, which can
 * number in thousands and make initial page rendering slow.
 */
@Pipe({ name: 'lazyOptions', pure: false })
export class LazyOptionsPipe implements PipeTransform, OnDestroy {
  private selectedOptionsOnly = true;
  private subscription: Subscription;

  constructor(
    private readonly select: MatSelect,
    private readonly cdr: ChangeDetectorRef
  ) {
    this.subscription = this.select.openedChange.subscribe(() => {
      this.selectedOptionsOnly = false;
    });

    // Preload after some time.
    requestIdleCallback(
      () => {
        this.selectedOptionsOnly = false;
        this.cdr.markForCheck();
      },
      { timeout: 1000 }
    );
  }

  transform<T>(values: T[]): T[] {
    if (!this.selectedOptionsOnly) {
      return values;
    }

    return values.filter((value) => {
      return this.select.compareWith(value, this.select.value);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

To use, just specify it in your template:

<mat-form-field appearance="fill">
  <mat-label>Select an option</mat-label>
  <mat-select [(value)]="selected">
    <mat-option>None</mat-option>
    <mat-option *ngFor="let value of values | lazyOptions" [value]="value"
      >{{value}}</mat-option
    >
  </mat-select>
</mat-form-field>
pookdeveloper commented 1 year ago

I have the same issue in angular material 15.2.1: if i have a ngif it very slow when i have table with a MatSelect with MatFormField column it is very slow:

with version 9 it works: https://stackblitz.com/edit/angular-material-table-infinite-load-xvpg32?file=src/app/table/table.component.ts

  <div *ngIf="formSubmitted">
    <fe-table-error></fe-table-error>
  </div>
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'fe-condor-table-error',
  templateUrl: './table-error.component.html',
  styleUrls: ['./table-error.component.scss']
})
export class TableErrorComponent implements OnInit, AfterViewInit {
  entity: any;

  displayedColumns = ['position', 'name', 'weight', 'symbol'];
  dataSource: any;
  datos: any = [];
  start: number = 0;
  limit: number = 1000;
  end: number = this.limit + this.start;
  selectedRowIndex: number = null;
  listFormControl = new FormControl();

  moreInfoList = [
    {
      name: 'Detalle del subcontrato',
    },
    {
      name: 'Detalle del contrato',
    },
  ];

  constructor() {
  }

  ngOnInit(): void {
    this.datos = Array.from({length: 500}, () => ({
      id: Math.floor(Math.random() * 100) + 1,
      position: 3,
      name: 'Lithium',
      weight: 6.941,
      symbol: 'Li',
    }));

    this.dataSource = this.getTableData(this.start, this.end);
    this.updateIndex();
  }

  onTableScroll(e: any) {
    console.log(e);
    const tableViewHeight = e.target.offsetHeight; // viewport
    const tableScrollHeight = e.target.scrollHeight; // length of all table
    const scrollLocation = e.target.scrollTop; // how far user scrolled

    // If the user has scrolled within 200px of the bottom, add more data
    const buffer = 200;
    const limit = tableScrollHeight - tableViewHeight - buffer;
    if (scrollLocation > limit) {
      let data = this.getTableData(this.start, this.end);
      this.dataSource = this.dataSource.concat(data);
      this.updateIndex();
    }
  }

  getTableData(start: any, end: any) {
    return this.datos.filter((value: any, index: any) => index >= start && index < end);
  }

  updateIndex() {
    this.start = this.end;
    this.end = this.limit + this.start;
  }

  selectedRow(row: any) {
    console.log('selectedRow', row);
  }

  ngAfterViewInit(): void {
    console.log('### ------------> performance.now() \n', performance.now());
    console.timeEnd('Execution Time');
  }
}

export interface Element {
  name: string;
  position: number;
  weight: number;
  symbol: string;
  age: number;
}

const ELEMENT_DATA: Element[] = [];
<div class="example-container mat-elevation-z8">
  <mat-table #table [dataSource]="dataSource" (scroll)="onTableScroll($event)">

    <!--- Note that these columns can be defined in any order.
          The actual rendered columns are set as a property on the row definition" -->

    <!-- Position Column -->
    <ng-container matColumnDef="position">
      <mat-header-cell *matHeaderCellDef> No.</mat-header-cell>
      <mat-cell *matCellDef="let element">
        {{element.position}}

        <button mat-raised-button color="primary">{{ "SEARCH" }}</button>

        <mat-form-field appearance="fill">
          <mat-label>ss</mat-label>
          <mat-select>
            <mat-option class="red-text" *ngFor="let el of moreInfoList" [value]="el">
              <a>{{ el.name }}</a>
            </mat-option>
          </mat-select>
        </mat-form-field>
      </mat-cell>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <mat-header-cell *matHeaderCellDef> Name</mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
    </ng-container>

    <!-- Weight Column -->
    <ng-container matColumnDef="weight">
      <mat-header-cell *matHeaderCellDef> Weight</mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.weight}} </mat-cell>
    </ng-container>

    <!-- Symbol Column -->
    <ng-container matColumnDef="symbol">
      <mat-header-cell *matHeaderCellDef> Symbol</mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.symbol}} </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns; let i= index" (click)="selectedRow(row)"
             [ngClass]="{'highlightTableColor': selectedRowIndex == i}"></mat-row>
  </mat-table>
</div>