Ionic 5: VirtualScroll and images caching

I am building an app (Ionic 5 + Angular 9 + Capacitor) where I have long lists of cards containing images and short description.
So, in this case I have to use virtual scroll because the pages with long lists are loading too slow otherwise.
The problem is that the same images (from outside url) are loading again every time you scroll down and up so, it’s really bad if you use mobile connection (3G etc.) and also I would like to use these images if you are offilne/loose the connection.

I have it solved somehow, however, if you scroll too fast down and up the right images are replaced with wrong ones. It is because of virtual scroll.

So, do you know any good solution which I could use for images caching and their offline use if I use virtual scroll?

First, I wanted to use ServiceWorker but it doesn’t work if you build an app on a device. See more about the issue here:

I have also found this plugin but it does not work with Ionic 5 and Capacitor.

So, my solution for now is something like that:

<ion-virtual-scroll class="scroll" [items]="_filteredEvents" approxItemHeight="126px">
    <app-event-list-card *virtualItem="let event" [event]="event" [networkStatus]="_networkStatus" style="opacity:1"></app-event-list-card>


ngOnChanges(changes: SimpleChanges) {
      Storage.get({key: 'img' +}).then((image) => {
        if (image.value) {
          this.event.image = image.value; // todo: do not assign a variable to event object
        } else if(this.networkStatus) {
          this.convertImageToBase64(this.event.image).then((dataUrl: string) => {
            Storage.set({key: 'img' +, value: dataUrl});
        } else {
          this.event.image = this.transparentImage;

  private async convertImageToBase64(url): Promise<any> {
    const response = await fetch(url);
    const blob = await response.blob();
    const result = new Promise((resolve, reject) => {
      if (blob.type === 'text/html') {
      } else {
        const reader = new FileReader();
        reader.onloadend = () => resolve(reader.result);
        reader.onerror = () => reject;

    return await result;


<ion-card *ngIf="event" [routerLink]="'/event-details/' +" [class]="event.cancelled == 1 ? 'event-card__card event-card__card--cancelled' : 'event-card__card'">
    <ion-card-content class="event-card__inner">
      <ion-grid class="ion-no-padding">
        <ion-row class="ion-align-items-center">
          <ion-col size="5">
            <ion-thumbnail class="event-card__thumb">
              <!--<ion-img [src]="event.image" (ionError)="loadDefaultImage($event)" alt="Image - {{}}"></ion-img>-->
              <img [src]="sanitizer.bypassSecurityTrustResourceUrl(event.image)" onerror="this.src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'" crossorigin="anonymous">
          <ion-col size="7" class="event-card__description ion-no-padding">
            <ion-row class="ion-text-center">
              <h2 class="event-card__title">{{ ( }}</h2>
            <ion-row class="ion-text-center">
              <h4 class="event-card__category">{{ event.event_type }}</h4>
            <ion-row class="ion-text-center">
              <h3 class="event-card__location">{{ event.location }}</h3>

I am using onChanges here because if you use onInit with virtual scroll it will be executed only about 10 times even if you have 100 items – virtual scroll is replacing the cards instead of creating new ones.