web-dev-qa-db-fra.com

Angular 4 ralentit dans le temps

J'ai une application angular 4.3.5 qui ralentit après avoir été utilisée pendant un certain temps (~ 20 minutes).

Mon scénario est comme:

  • API Rest et statique angular html/css/js fonctionnant sur RaspberryPi B 3
  • ~ 30 RaspberryPI B 3 accédant à une application statique angular via Chromium (versions 58 et 60)

Ce qui se passe:

  • Les requêtes HTTP de l'Angular sont devenues plus lentes au fil du temps. Exemple: de ~ 100 ms à ~ 2 secondes

Informations supplémentaires:

  • Si j'appuie sur F5 sur Chrome, l'application Angular revient à la normale
  • Angular utilise ce modèle https://themeforest.net/item/primer-angular-2-material-design-admin-template/19228165
  • Angular utilise une application Google Chrome/Chromium, que j'ai écrite, pour la communication avec un Arduino via le port série (API Chrome: chrome.runtime.sendMessage, chrome.runtime.connect et chrome.serial)
  • Le client, RaspberryPi, dispose de ressources disponibles (CPU et mémoire) lorsque l'application ralentit
  • L'application angulaire ne stocke presque rien sur le navigateur

Le composant qui présente le problème est le suivant:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import 'rxjs/add/operator/takeUntil';
import { Subject } from 'rxjs/Subject';

import { SweetAlertService } from 'ng2-cli-sweetalert2';

import { ApiService } from '.././api.service';
import { NFCService } from '.././nfc.service';

@Component({
  selector: 'app-menu',
  templateUrl: './menu.component.html',
  styleUrls: ['./menu.component.scss']
})
export class MenuComponent implements OnInit, OnDestroy {

  private ngUnsubscribe: Subject<void> = new Subject<void>();

  cardId: string;
  userId: string;
  userName: string;
  is_secure_bar: boolean = false;

  requestInProgress = false;

  userBalance: number = 0;

  step: number = 1;
  // showCheckout: boolean = false;

  categories = new Array();
  subcategories = new Array();
  products = new Array();

  cartItems = new Array();

  countCartItems: number = 0;
  totalCartValue: number = 0;

  table_scroller;
  table_scroller_height;
  show_scroller_btns = false;

  constructor(
    public router: Router,
    public route: ActivatedRoute,
    private _nfcService: NFCService,
    private _apiService: ApiService,
    private _swal: SweetAlertService
  ) { }

  ngOnInit() {
    var account = localStorage.getItem('account');
    if (account) {
      // set variable to catch user data
      // JSON.parse(
    } else {
      this.router.navigate(['login']);
    }

    this.route.params
    .takeUntil(this.ngUnsubscribe)
    .subscribe(params => {
      this.cardId = params.id;
      this._apiService.getCardUser(params.id)
      .takeUntil(this.ngUnsubscribe)
      .subscribe(
        response => {
          // SUCCESS
          this.userId = response.data[0].uuid;
          this.userBalance = response.data[0].balance;
          this.userName = response.data[0].name;
        },
        error => {
          // ERROR
          console.log('Failed ;(', error);
        }
      );
    });

    this.getEvents()
    .takeUntil(this.ngUnsubscribe)
    .subscribe(
      response => {
        if (response.data[0].options.sales_auth_after_buy_is_required) {
          this.is_secure_bar = true;
        }
      },
      error => {
        console.log('Erro ao verificar Evento.')
      }
    );

    var categories = localStorage.getItem('cache_categories');
    if (categories) {
      this.categories = JSON.parse(categories);
    } else {
      // this.getCategories();
      this.getCategoriesP()
    }

  }

  //@felipe_todo
  getEvents()
  {
    return this._apiService.getEvents();

    //COMO FAZER LOGOUT ABAIXO
    //localStorage.clear();
  }

  getCategories() {
    this._apiService.getProductsCategories()
      .takeUntil(this.ngUnsubscribe)
      .subscribe(response => {
        // SUCCESS
        this.categories = response.data;
        localStorage.setItem('cache_categories', JSON.stringify(this.categories));
      }, error => {
        // ERROR
        console.log('Failed ;(', error);
      });
  }

  getCategoriesP() {
    let categories;
    this._apiService.getCategories()
      .then(response => categories = response)
      .then(() => {
        this.categories = categories;
        console.log(categories);
      });
  }

  categorySelected(item) {
    this.step = 2;

    var subcategories = localStorage.getItem('cache_subcategories_' + item.uuid);
    if (subcategories) {
      this.subcategories = JSON.parse(subcategories);
    } else {
      // this.getSubcategories(item.uuid);
      this.getSubcategoriesP(item.uuid);
    }
  }

  getSubcategories(uuid) {
    this._apiService.getProductsSubcategories(uuid)
      .takeUntil(this.ngUnsubscribe)
      .subscribe(response => {
        // SUCCESS
        this.subcategories = response.data;
        localStorage.setItem('cache_subcategories_' + uuid, JSON.stringify(this.subcategories));
      }, error => {
        // ERROR
        console.log('Failed ;(', error);
      });
  }

  getSubcategoriesP(uuid) {
    let subcategories;
    this._apiService.getSubcategories(uuid)
      .then(response => subcategories = response)
      .then(() => {
        this.subcategories = subcategories;
        console.log(subcategories);
      });
  }

  subCategorySelected(item) {
    this.step = 3;

    var products = localStorage.getItem('cache_products_' + item.uuid);
    if (products) {
      this.products = JSON.parse(products);
    } else {
      // this.getProducts(item.uuid);
      this.getProductsP(item.uuid);
    }
  }

  getProducts(uuid) {
    this._apiService.getProducts(uuid)
      .takeUntil(this.ngUnsubscribe)
      .subscribe(response => {
        // SUCCESS
        this.products = response.data;
        localStorage.setItem('cache_products_' + uuid, JSON.stringify(this.products));
      }, error => {
        // ERROR
        console.log('Failed ;(', error);
      });
  }

  getProductsP(uuid) {
    let products;
    this._apiService.getProductList(uuid)
      .then(response => products = response)
      .then(() => {
        this.products = products;
        console.log(products);
      });
  }

  addToCard(product) {
    var existentItems = this.cartItems.filter(function(item) {
      return item.uuid === product.uuid
    });

    if (existentItems.length) {
      existentItems[0].quantity += 1
    } else {
      product.quantity = 1;
      this.cartItems.unshift(product);
    }
    let that = this;
    this.calculateTotal();
    setTimeout(function(){
      that.setScroller();
    }, 300);
  }

  removeProduct(index) {
    let product = this.cartItems[index]
    var existentItems = this.cartItems.filter(function(item) {
      return item.uuid === product.uuid
    });

    if (existentItems.length) {
      existentItems[0].quantity -= 1
      if (existentItems[0].quantity == 0) {
        this.cartItems.splice(index, 1);
      }
    } else {
      product.quantity = 1;
      this.cartItems.splice(index, 1);
    }

    this.calculateTotal();
    let that = this;
    setTimeout(function(){
      if (that.table_scroller.offsetHeight < 270) {
        that.show_scroller_btns = false;
      }
    }, 300);
  }

  calculateTotal() {
    this.countCartItems = 0;
    this.totalCartValue = 0;

    var that = this;
    this.cartItems.forEach(function(item) {
      that.countCartItems += item.quantity;
      that.totalCartValue += item.value * item.quantity;
    });
  }

  backStep() {
    if (this.step == 2) {
      this.subcategories = new Array();
    } else if (this.step == 3) {
      this.products = new Array();
    }

    this.step--;
  }

  setScroller() {
    if (this.cartItems.length) {
      if (!this.table_scroller) {
        this.table_scroller = document.querySelector('#table-scroller');
      }else {
        console.log(this.table_scroller.offsetHeight)
        if (this.table_scroller.offsetHeight >= 270) {
          this.show_scroller_btns = true;
        } else {
          this.show_scroller_btns = false;
        }
      }
    }
  }

  scrollDown() {
    (<HTMLElement>this.table_scroller).scrollTop = (<HTMLElement>this.table_scroller).scrollTop+50;
  }

  scrollUp() {
    (<HTMLElement>this.table_scroller).scrollTop = (<HTMLElement>this.table_scroller).scrollTop-50;
  }

  confirmDebit() {

    if (this.requestInProgress) return;

    if (this.userBalance < this.totalCartValue) {
      this._swal.error({ title: 'Salto Insuficiente', text: 'Este cliente não possui saldo suficiente para essa operação.' });
      return;
    }

    this.requestInProgress = true;

    var order = {
      card_uuid: this.cardId,
      event_uuid: 'c7b5bd69-c2b5-4226-b043-ccbf91be0ba8',
      products: this.cartItems
    };

    let is_secure_bar = this.is_secure_bar;

    this._apiService.postOrder(order)
       .takeUntil(this.ngUnsubscribe)
         .subscribe(response => {
        console.log('Success');
        // this.router.navigate(['customer', this.userId]);

        let that = this;
        this._swal.success({
          title: 'Debito Efetuado',
          text: 'O débito foi efetuado com sucesso',
          showCancelButton: false,
          confirmButtonText: 'OK',
          allowOutsideClick: false,
        }).then(function(success) {
          console.log("Clicked confirm");
          if (is_secure_bar) {
            that.logout();
          } else {
            that.router.navigate(['card']);
          }
        });

        this.requestInProgress = false;

      }, error => {
        // ERROR
        console.log('Request Failed ;(', error);

        if (error.status !== 0) {
          // TODO: Should display error message if available!
          this._swal.error({ title: 'Erro', text: 'Ocorreu um erro inesperado ao conectar-se ao servidor de acesso.' });
        } else {
          this._swal.error({ title: 'Erro', text: 'Não foi possível conectar-se ao servidor de acesso. Por favor verifique sua conexão.' });
        }

        this.requestInProgress = false;
      }
      );
  }

  logout() {
    let that = this;
    localStorage.clear();
    that.router.navigate(['login']);
  }

  clearCheckout() {
    this.cartItems = new Array();
    this.calculateTotal();

    this.router.navigate(['card']);
  }

    ngOnDestroy() {
      console.log('uhul')
        this.ngUnsubscribe.next();
        this.ngUnsubscribe.complete();
    }

}

Les méthodes qui présentent une lenteur chaque fois que nous accédons au composant sont:

getCategories () getSubcategories (uuid) getProducts (uuid) confirmDebit ()

À des fins de test, nous avons créé une nouvelle version pour chacune de ces méthodes, cette fois avec des promesses:

getCategoriesP () getSubcategoriesP (uuid) getProductsP (uuid)

Quelle que soit la version de la méthode appelée, le même problème se produit.

18
Diego Andrade

Si vous exécutez votre application en tant que SPA (application à page unique), cela peut être l'une des causes de la dégradation des performances au fil du temps.

Dans SPA, le DOM devient plus lourd à chaque fois que l'utilisateur visite une nouvelle page . Par conséquent, vous devez travailler sur la façon de garder le DOM léger.

Voici les principales raisons pour lesquelles j'ai trouvé une amélioration significative des performances de mon application.

Vérifiez les points ci-dessous:

  • Si vous avez utilisé le contrôle des onglets, ne chargez que le contenu des onglets actifs et aucun autre contenu des onglets ne devrait exister sur le DOM.
  • Si une fenêtre contextuelle va se charger, assurez-vous qu'elle ne charge le corps qu'à son ouverture.
  • Composants courants comme confirmation popup et message alert doit être défini une seule fois et accessible à l'échelle mondiale.
  • * ngPour appliquer avec trackby ( https://netbasal.com/angular-2-improve-performance-with-trackby-cc147b5104e5 )

Une fois le service terminé, appelez destroy sur tous les objets: (ci-dessous est l'exemple d'appel d'un service)

import { Subject } from 'rxjs/Subject'
import 'rxjs/add/operator/takeUntil';

ngOnDestroy() {        
    this.ngUnsubscribe.next(true);
    this.ngUnsubscribe.complete();
}

this.frameworkService.ExecuteDataSource().takeUntil(this.ngUnsubscribe).subscribe((data: any) => {
    console.log(data);
});

Reportez-vous aux liens ci-dessous pour plus de détails:

https://medium.com/paramsingh-66174/catalysing-your-angular-4-app-performance-9211979075f6

Vous ne savez pas si cela résoudra votre problème de performances, mais cela pourrait être un pas dans la bonne direction.

Vous créez un nouvel abonnement chaque fois qu'un paramètre d'itinéraire change, vous pourriez donc vous retrouver avec beaucoup d'abonnements:

this.route.params
.takeUntil(this.ngUnsubscribe)
.subscribe(params => {
  this.cardId = params.id;
  this._apiService.getCardUser(params.id)
  .takeUntil(this.ngUnsubscribe)
  .subscribe(
    response => {
      // SUCCESS
      this.userId = response.data[0].uuid;
      this.userBalance = response.data[0].balance;
      this.userName = response.data[0].name;
    },
    error => {
      // ERROR
      console.log('Failed ;(', error);
    }
  );
});

Je pense que vous feriez mieux d'utiliser switchMap, de cette façon, il n'y aura qu'un seul abonnement. Quelque chose comme:

this.route.params .switchMap(params => { this.cardId = params.id; return this._apiService.getCardUser(params.id) }) .takeUntil(this.ngUnsubscribe) .subscribe( response => { // SUCCESS this.userId = response.data[0].uuid; this.userBalance = response.data[0].balance; this.userName = response.data[0].name; }, error => { // ERROR console.log('Failed ;(', error); } ); });

2
David Bulté

Je pense que le problème se situe quelque part dans ce mécanisme d'abonnement dans vos méthodes get

(getProducts, getCategories etc.)

Comment créez-vous cet observable qui est renvoyé des appels à votre api-Service? Après avoir appelé votre service api, vous vous abonnez à la valeur de retour de cet appel. Est-ce la réponse originale de la requête http? Ou est-ce un devoir que vous avez créé vous-même?

En général, vous n'avez pas besoin d'appeler désabonnement sur les appels http en angulaire, comme cela est décrit ici:

Devez-vous vous désinscrire de Angular 2 appels http pour éviter les fuites de mémoire?

Mais au cas où vous ne passeriez pas par cet observable http original, mais créeriez votre propre observable, alors vous pourriez avoir besoin de le nettoyer vous-même.

Peut-être pouvez-vous publier un code de votre service api? Comment créez-vous ces promesses?

Une autre chose : Appelez-vous votre méthode getProducts avec un uuid différent à chaque fois? Vous écririez une nouvelle entrée dans localStorage avec chaque uuid unique avec lequel vous appelez cette méthode

2
Tobias Gassmann