Click delay Browser VS Device

Hi, i’m facing a click delay problem while test my app on real device (IOS/Android).
I have a VirtualList of 5 items according to this template:

<ion-list [virtualScroll]="feed.posts" approxItemHeight="100px"  no-lines>
  <div class="feed-item" *virtualItem="let post">
    <ion-card>
      <ion-row no-padding class="actions-row">
        <ion-col no-padding width-50 text-left>
          <button tappable class="action-button" ion-button clear small icon-left (click)="goToPost(post.id)">
            <ion-icon name='pricetags'></ion-icon>
              {{post.nombrePromotion}} Promotion(s)
          </button>
        </ion-col>
        ...
      </ion-row>
    <ion-card>
  </div>
</ion-list>

While testing on browser, clicks are very fast responsive but when I test the app on Iphone 5C or Android devices (or emulator) there is to much delay (like 2s).

Here you can see videos with click delay:
ON BROWSER:

ON IPHONE 5C:

How can i improve clicks speed on device/emulator?

Thank you

Info:

Ionic Framework: 2.2.0
Ionic Native: ^3.5.0
Ionic App Scripts: 1.1.4
Angular Core: 2.4.8
Angular Compiler CLI: 2.4.8
Node: 7.7.3
OS Platform: macOS Sierra
Navigator Platform: MacIntel

Hi,

did you try it without that tappable directive on the button? Maybe updating to a newer version of the framework can solve this for you.

Yes I tried without tappable but it’s the same. You mean that I should update ionic to V3? Will it generate many of changes in the code?

I’ve migrated a couple of projects and I didn’t have to made many changes. Please be aware of the lazy loading, it’s not in particular necessary and since it has some issues you might want to skip that.

I upgraded ionic to latest version 3.2.0 and have the same issue with click delay… Can someone help me with that please? Any tips? Because it brings a very no-natif feeling and the app is pretty unusable :worried:

It could be a memory issue (since you’re running on an iPhone 5c), but it could also be something on the page you’re trying to navigate to. Maybe there’s a lot of stuff happening there, but it’s hard to tell without seeing some code of that particular page.

Here is my html:

<ion-header>
  <ion-navbar>
  <ion-buttons  end >
      <button [hidden]="!showScrollTop" ion-button style="font-size:24px;" (click)="scrollToTop()">
       <ion-icon name="arrow-up"></ion-icon>
      </button>
    </ion-buttons>
    <ion-title>
      <span>Garages</span>
    </ion-title>

  </ion-navbar>
</ion-header>





<ion-content class="feed-content">

   <ion-refresher (ionRefresh)="doRefresh($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>


    <ion-toolbar class="search-toolbar border-bottom">
    <ion-buttons start>
      <button ion-button icon-only (click)="geolocateMe()" class="geolocation-button" >
        <ion-icon name="locate"></ion-icon>
      </button>
    </ion-buttons>
    <span class="actual-pos" placeholder="" >Position actuelle: {{adresse_actuelle}}</span>
  </ion-toolbar>

  <form [formGroup]="rangeForm">
    <ion-list class="range-list">
      <ion-item class="range-item single-range">
        <ion-label>
          <h2 class="range-label">Distance maximale:</h2>
          <h3 class="range-value">{{rangeForm.controls.single.value}}km</h3>
        </ion-label>
        <ion-range formControlName="single" (ionChange)="rangeChange($event)" min="1" max="50" step="1" snaps="true" pin="false" ></ion-range>

      </ion-item>
    </ion-list>
  </form>


  <ion-col [hidden]="!rangeUpdated" no-padding width-100 class="refresh-arrow-col">
    <div  > 
      <p class="refresh-text">Tirez pour rafraichir les résultats</p>
      <ion-icon class="refresh-arrow" name="arrow-down"></ion-icon>
    </div>
  </ion-col>

  <ion-col no-padding width-100 class="refresh-arrow-col">
      <p class="refresh-text" style="padding-bottom:15px;">Garages à proximité: <strong>{{feed?.posts?.length}}</strong></p>
  </ion-col>



 <ion-list [virtualScroll]="feed.posts" approxItemHeight="100px"  no-lines>
  <div class="feed-item" *virtualItem="let post" style="width: 100%">
    <ion-card [class.premium]="post.premium==1">

      
      <ion-row tappable class="user-main-data-row" (click)="goToGarage(post.garage.id)" >
         <div *ngIf="post.premium==1">
           <span class="premium-text">SPONSORISÉ</span>
      </div> 
        <ion-col no-padding width-33>

          <preload-image class="user-image" [ratio]="{w:1, h:1}" src="http://garageadvisor.pre-production.ovh/upload/{{post.garage.logo}}" alt=""  ></preload-image>
        </ion-col>
        <ion-col class="center" no-padding width-67 style="padding-left:5px;">
          <p class="item-title">{{post.garage.nom}}</p>

 
              <p class="avis-list">Note: <span class="note">{{post.garage.moyenneAvis}}/5</span> ({{post.garage.nbAvis}} Avis) </p>

        </ion-col>
      </ion-row>

      <ion-card-content tappable (click)="goToGarage(post.garage.id)">
   
                 <ion-list class="details-list" no-lines>
                  <ion-item class="place-location">
                    <ion-avatar item-left>
                      <ion-icon name="pin"></ion-icon>
                    </ion-avatar>
                    <div *ngIf="post.distance<1">
                      <span class="location-text">{{post.garage.adresse}} (à {{post.distance*1000 | num }} m)</span>
                    </div> 
                    <div *ngIf="post.distance>1">
                      <span class="location-text">{{post.garage.adresse}} (à {{post.distance | num }} km)</span>
                    </div> 
                  </ion-item>
                </ion-list>
      </ion-card-content>
      <ion-row no-padding class="actions-row">
        <ion-col no-padding width-50 text-left>
          <button tappable class="action-button" ion-button clear small icon-left (click)="goToGarage(post.garage.id)">
            <ion-icon name='pricetags'></ion-icon>
            {{post.nombrePromotion}} Promotion(s)
          </button>
        </ion-col>
        <ion-col  no-padding width-45 text-right >
          <button  class="action-button" ion-button clear small icon-left (click)="navigate(post.garage.adresse)">
            <ion-icon name='navigate'></ion-icon>
            Itinéraire
          </button>
        </ion-col>

      </ion-row>
    </ion-card>
    <div style="height:12px"></div>
  </div>
  </ion-list>
</ion-content>

And there the ts file:

import { Component, ViewChild, ChangeDetectorRef } from '@angular/core';
import { Content } from 'ionic-angular';
import { Geolocation } from '@ionic-native/geolocation';
import { NavController, NavParams, LoadingController, AlertController, ToastController } from 'ionic-angular';
import { LaunchNavigator, LaunchNavigatorOptions } from '@ionic-native/launch-navigator';
import { FormGroup, FormControl } from '@angular/forms';
import { ProfilePage } from '../profile/profile';
import { ContactCardPage } from '../contact-card/contact-card';
import 'rxjs/Rx';
import { FeedPostModel } from './feed.model';
import { FeedModel } from './feed.model';
import { FeedService } from './feed.service';
import { SocialSharing} from '@ionic-native/social-sharing';
import { WalkthroughPage } from '../walkthrough/walkthrough';
import {App} from 'ionic-angular';
import { NativeStorage } from '@ionic-native/native-storage';


import { LoginService } from '../login/login.service';


@Component({
  selector: 'feed-page',
  templateUrl: 'feed.html'
})
export class FeedPage {
  feed: FeedModel = new FeedModel();
  rangeForm: any;
  loading: any;
  page: number;
  next: boolean;
  rangeUpdated:boolean;
  latitude:any;
  longitude:any;
  adresse_actuelle:string;
  range_distance:any;
  showScrollTop:boolean;

  constructor(
    public app: App,
    public nav: NavController,
    public feedService: FeedService,
    public navParams: NavParams,
    public loadingCtrl: LoadingController,
    public alertCtrl: AlertController,
    private launchNavigator: LaunchNavigator,
    public loginService: LoginService,
    public toastCtrl: ToastController,
    private geolocation: Geolocation,
    private socialSharing: SocialSharing,
    public nativeStorage: NativeStorage,
    private changeDetectorRef: ChangeDetectorRef

  ) {

    this.loading = this.loadingCtrl.create();

    this.page=0;

    this.next=false;

    this.rangeUpdated=false;

    if (localStorage.getItem("range_distance"))this.range_distance=localStorage.getItem("range_distance");
    else this.range_distance=25;

    this.rangeForm = new FormGroup({
      single: new FormControl(this.range_distance)
    });

    this.latitude=0; 
    this.longitude=0;
    this.adresse_actuelle=localStorage.getItem("adresse-actuelle");
    this.showScrollTop=false;

    
  }


  ionViewDidLoad() {

    let loading = this.loadingCtrl.create({
      content: 'Mise à jour de votre position'
    });
    loading.present();
    let posOptions = {enableHighAccuracy: true};
    this.geolocation.getCurrentPosition(posOptions).then((position) => {
          localStorage.setItem("latitude", String(position.coords.latitude));     // Ajoute la latitude
          localStorage.setItem("longitude", String(position.coords.longitude));   // Ajoute la longitude
          loading.dismiss().catch(() => {});
          let loading2 = this.loadingCtrl.create();
          loading2.present();
          this.loginService.
          getAdresse()            
          .then(data => {
                  localStorage.setItem("adresse-actuelle", data[0].long_name+" "+data[1].long_name+", "+data[2].long_name);
                  this.adresse_actuelle=data[0].long_name+" "+data[1].long_name+", "+data[2].long_name;
                  this.feedService
                  .getGarages(0, this.rangeForm.controls.single.value, 1000)
                  .then(data => {
                        this.feed.posts = data.Content as FeedPostModel[];
                        this.next = data.Next;
                        loading2.dismiss().catch(() => {});
                  }).catch(error => { 
                    loading2.dismiss().catch(() => {});
                    this.errorAlert(error);
                  });
          }).catch((error) => {
          loading2.dismiss().catch(() => {});
          this.errorAlert("GoogleMap");
          });
          
          loading.dismiss().catch(() => {});

    }).catch((error) => {
      loading.dismiss().catch(() => {});
      this.errorAlert("GPS");
    });



  }

  @ViewChild(Content) content: Content;

  scrollToTop() {
    this.content.scrollToTop(2000);
    this.showScrollTop=false;
  }

  ngAfterViewInit() {
    this.content.ionScrollEnd.subscribe((data)=>{
      if (data.scrollTop > 1000) {
        this.showScrollTop=true;
      } else {
        this.showScrollTop=false;
      }
      this.changeDetectorRef.detectChanges();
    });
  }

  ionViewDidEnter () {
      if (this.adresse_actuelle!=localStorage.getItem("adresse-actuelle"))this.rangeUpdated=true;
      this.adresse_actuelle=localStorage.getItem("adresse-actuelle");
  }

   doInfinite(infiniteScroll) {
      this.page++;
      this.feedService
        .getGarages(this.page, this.rangeForm.controls.single.value, 10)
        .then(data => {
          this.feed.posts = this.feed.posts.concat(data.Content as FeedPostModel[]);
          this.next = data.Next;
          infiniteScroll.complete();
        }).catch(error => { 
        this.loading.dismiss().catch(() => {});
        this.errorAlert(error);
      });
  }

  goToProfile(event, item) {
    this.nav.push(ProfilePage, {
      user: item
    });
  }

  getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  sharePost(post) {
   //this code is to use the social sharing plugin
   // message, subject, file, url
   this.socialSharing.share(post.description, post.title, post.image)
   .then(() => {
 
   })
   .catch(() => {
  
   });
 }


  rangeChange(range: Range) {
    this.rangeUpdated=true;  
    localStorage.setItem("range_distance", this.rangeForm.controls.single.value);

  }

  goToGarage(id_garage: any) {
      this.nav.push(ContactCardPage, { id_garage: id_garage });
  }

  doRefresh(refresher) {
    this.page=0;
    this.feedService
    .getGarages(0, this.rangeForm.controls.single.value, 1000)
    .then(data => {
      this.feed.posts = data.Content as FeedPostModel[];
      this.next = data.Next;
      this.rangeUpdated=false;
      refresher.complete();
    }).catch(error => { 
      this.loading.dismiss().catch(() => {});
      this.errorAlert(error);
    });
  }


  navigate(adresse){
    let options: LaunchNavigatorOptions = {
      //start: [parseFloat(localStorage.getItem("latitude")),parseFloat(localStorage.getItem("longitude"))],
      appSelectionDialogHeader: "Veuillez choisir l'application GPS",
      appSelectionCancelButton: "Annuler"
    };

  this.launchNavigator.navigate(adresse, options)
    .then(
      success => console.log('Launched navigator'),
      error => console.log('Error launching navigator', error)
    );


  }

  geolocateMe(){
    let loading = this.loadingCtrl.create();
    loading.present();
    let posOptions = {enableHighAccuracy: true};
    this.geolocation.getCurrentPosition(posOptions).then((position) => {
    localStorage.setItem("latitude", String(position.coords.latitude));     // Ajoute la latitude
    localStorage.setItem("longitude", String(position.coords.longitude));   // Ajoute la longitude
   
    this.loginService.
    getAdresse()            
    .then(data => {
      localStorage.setItem("adresse-actuelle", data[0].long_name+" "+data[1].long_name+", "+data[2].long_name);
      this.adresse_actuelle=data[0].long_name+" "+data[1].long_name+", "+data[2].long_name;
    }).catch((error) => {
    loading.dismiss().catch(() => {});
    });
    this.rangeUpdated=true;
    let toast = this.toastCtrl.create({
    message: "Votre position a bien été mise à jour.",
    duration: 3000,
    position: 'bottom'
    });
    toast.present();
    loading.dismiss().catch(() => {});

    }).catch((error) => {
      loading.dismiss().catch(() => {});
      this.errorAlert("GPS");
    });
  }



  errorAlert(error) {
    let message:string;
    if (error=="GPS"){ 
      message="Veuillez autoriser GarageAdvisor à accéder au service de localisation de votre téléphone et réessayez.";
    } else if (error=="GoogleMap") { 
      message="GoogleMap n'est pas accèssible. Veuillez réessayer ultérieurement."; 
    } else {
      if (error.status==400){
          message=error.json().Error;
      } else if (error.status==401){
          message="Votre session a expiré. Veuillez vous reconnecter."
      } else if (error.status==403){
          if (error.json().Error=="No_Enabled"){
            message="Votre compte a été suspendu. Veuillez contacter l'administrateur."
          } else  {
            message="Vous n'avez pas les droits d'accèder à cette page."
          }
      } else if (error.status==404){
          if (error.json().Error=="Not_Found_Garage"){
            message="Ce garage n'existe pas ou a été supprimé.";
          } else if (error.json().Error=="Not_Found_Promotion"){
            message="Cette promotion n'existe pas ou a été supprimée.";
          } else if (error.json().Error=="Not_Found_Member"){
            message="Votre compte n'existe pas ou a été supprimé.";
          } else if (error.json().Error=="Not_Found_Vente"){
            message="Cette vente n'existe pas ou a été supprimée.";
          } else if (error.json().Error=="Not_Found_Avis"){
            message="Cet avis n'existe pas ou a été supprimé.";
          } else message="Page introuvable.";
      } else message="Une erreur interne est survenue. Veuillez réessayer ultérierement."
    }

    let alert = this.alertCtrl.create({
      title: 'Erreur',
      subTitle: message,
      buttons: [
      {
        text: 'Ok',
        role: 'cancel',
        handler: () => {
          if ((error.status==401) || (error.status==500) || ((error.status==403) && (error.json().Error=="No_Enabled"))){
            localStorage.setItem("token","");
            localStorage.setItem("email","");
            localStorage.setItem("id","");
            this.nativeStorage.clear();
            this.app.getRootNav().setRoot(WalkthroughPage);
          }
          if ((error.status==404) || ((error.status==403) && (error.json().Error!="No_Enabled")) ){
            this.nav.pop();
          }
        }
      }
    ]
    });
    alert.present();
  }



}

Do you see something strange? It shouldn’t be a memory problem (or is so it is a ionic problem) because i have a virtual list of only 50-100 items. And if it is a memory problem how can I detect it? Also you can test the app in ionic view but it is really slow: C676D0D8

Somebody can help me please?

Seems like I have the same issue after updating to 3.2.0 with an iPhone 6… Any ideas yet? I’m not even sure if its the 300ms browser delay which strikes here or anything else (it feels like 1s, not 300ms).
I do not experience this on android or desktop…

I solved it by switching to WKWebview in iOS http://blog.ionic.io/cordova-ios-performance-improvements-drop-in-speed-with-wkwebview/

In the video i already use WKWebview… So my problem should be not the same :disappointed:

are you running with the --prod flag?

Yep, it provides the same result…

I notice that you’re binding click events to elements other than buttons or <a> tags. This was my issue when I was getting the 300ms delay. Apparently there is code in ionic that addresses the 300ms delay issue, but only for these two elements. See this posting for a little more detail. I had been using divs for click events and getting the delay. I changed the divs to <a> elements and the 300ms delay was resolved for my app. FYI.

In the video i perform the click on a <button> so this is not my issue. Thank you for trying to help me…

Can I share my git project with someone of the Ionic Team?

Related github issue: https://github.com/driftyco/ionic/issues/11668

Hi, I’m facing something similar and found this thread.
These issues are common to many webapps / hybrid apps and there are many reasons behind that “slow response” you get here and there.
As a general workaround I would suggest using a loading spinner (ie https://ionicframework.com/docs/api/components/loading/LoadingController/) to notify the user that something is going to happen and then dismiss once the “long” operation has completed.
In more detail the “present” method of the LoadingController is a promise therefore you can wait for the loader to appear (this gives a quick feedback to the user) and after that you can launch the “long” operation.
Hope it helps!

Please change {click} to {tap}. This will do the trick.