中安拓也のブログ

プログラミングについて書くブログ。 Twitterやってます @l08084

【Ionic v5】スライドの枚数が多いSlidesのパフォーマンスを改善する

f:id:l08084:20210418140904g:plain
スライド画面

はじめに

Ionicでモバイルアプリケーションを作成している時に、30枚以上のスライドを搭載しているSlidesを使用した画面において、スライドで表示しているボタンやラベルが時々消えてしまう障害を検知したことがあります。

今回は、上記のようなスライドの枚数が多いことで発生するパフォーマンス起因の障害を解消するための方法について説明します。

環境

TypeScriptベースのフレームワークであるAngularと、iOS/AndroidのハイブリッドモバイルアプリケーションのフレームワークであるIonicを使用しています。

ionic infoコマンドの実行結果

$ ionic info

Ionic:

   Ionic CLI                     : 6.11.8 (/usr/local/lib/node_modules/@ionic/cli)
   Ionic Framework               : @ionic/angular 5.6.4
   @angular-devkit/build-angular : 0.1102.9
   @angular-devkit/schematics    : 11.2.9
   @angular/cli                  : 11.2.9
   @ionic/angular-toolkit        : 3.1.1

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   NodeJS : v12.13.1 (/usr/local/bin/node)
   npm    : 6.14.12
   OS     : macOS Catalina

今回使用するサンプルアプリについて

10枚のスライドを表示する下記のサンプルアプリを使用して、パフォーマンスの改善案について説明します。

f:id:l08084:20210418140904g:plain
今回使用するサンプルアプリ

10枚のスライドを表示する画面のテンプレートファイルになります。<ion-slides>の中に<ion-slide>が10件含まれていることがわかります。

home.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title> スライド </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <!-- スライドが10枚ある -->
  <ion-slides #slides>
    <ion-slide>
      <app-page-one></app-page-one>
    </ion-slide>
    <ion-slide>
      <app-page-two></app-page-two>
    </ion-slide>
    <ion-slide>
      <app-page-three></app-page-three>
    </ion-slide>
    <ion-slide>
      <app-page-four></app-page-four>
    </ion-slide>
    <ion-slide>
      <app-page-five></app-page-five>
    </ion-slide>
    <ion-slide>
      <app-page-six></app-page-six>
    </ion-slide>
    <ion-slide>
      <app-page-seven></app-page-seven>
    </ion-slide>
    <ion-slide>
      <app-page-eight></app-page-eight>
    </ion-slide>
    <ion-slide>
      <app-page-nine></app-page-nine>
    </ion-slide>
    <ion-slide>
      <app-page-ten></app-page-ten>
    </ion-slide>
  </ion-slides>
  <ion-button (click)="swipeNext()" expand="block">次のスライド</ion-button>
  <ion-button (click)="swipePrev()" expand="block">前のスライド</ion-button>
</ion-content>

10枚のスライドを表示する画面のコンポーネントクラスになります。

home.page.ts

import { Component, ViewChild } from '@angular/core';
import { IonSlides } from '@ionic/angular';

/**
 * スライドを10枚表示する画面
 *
 * @export
 * @class HomePage
 */
@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  @ViewChild('slides') slides: IonSlides;

  constructor() {}

  /**
   * 次のスライドに進む
   *
   * @memberof HomePage
   */
  public swipeNext(): void {
    this.slides.slideNext();
  }

  /**
   * 前のスライドに戻る
   *
   * @memberof HomePage
   */
  public swipePrev(): void {
    this.slides.slidePrev();
  }
}

スライドのパフォーマンスを改善する方法

搭載しているスライドの枚数が多いスライドのパフォーマンスを改善する方法について説明します。今回、説明する方法は下記の二つです。

  • 案1: バーチャルスライド

    • Angularのバーチャルスクロール機能のように、現在表示している部分のみDOM要素を生成することによって、一度に生成するDOM要素を減らし、パフォーマンスを改善します。具体的には、現在表示しているスライドとその前後のスライドだけをDOM要素として生成するようにします
  • 案2: Lazy Loading(遅延読み込み)

    • 一度に全てのスライドを表示しようとするのではなく、最初に表示する必要のあるスライドだけを先に表示して、残りのスライドは遅延させながら徐々に表示させることでパフォーマンスへの負担を減らすという方式になります

案1: バーチャルスライド

先ほどのサンプルアプリのコードを修正して、パフォーマンスの改善について説明します。まずはバーチャルスライド方式から

home.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title> スライド </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <!-- スライドが10枚ある -->
  <ion-slides
    #slides
    (ionSlideNextEnd)="loadNext()"
    (ionSlidePrevEnd)="loadPrev()"
  >
    <ion-slide>
      <app-page-one *ngIf="isVisibleSlide[0]"></app-page-one>
    </ion-slide>
    <ion-slide>
      <app-page-two *ngIf="isVisibleSlide[1]"></app-page-two>
    </ion-slide>
    <ion-slide>
      <app-page-three *ngIf="isVisibleSlide[2]"></app-page-three>
    </ion-slide>
    <ion-slide>
      <app-page-four *ngIf="isVisibleSlide[3]"></app-page-four>
    </ion-slide>
    <ion-slide>
      <app-page-five *ngIf="isVisibleSlide[4]"></app-page-five>
    </ion-slide>
    <ion-slide>
      <app-page-six *ngIf="isVisibleSlide[5]"></app-page-six>
    </ion-slide>
    <ion-slide>
      <app-page-seven *ngIf="isVisibleSlide[6]"></app-page-seven>
    </ion-slide>
    <ion-slide>
      <app-page-eight *ngIf="isVisibleSlide[7]"></app-page-eight>
    </ion-slide>
    <ion-slide>
      <app-page-nine *ngIf="isVisibleSlide[8]"></app-page-nine>
    </ion-slide>
    <ion-slide>
      <app-page-ten *ngIf="isVisibleSlide[9]"></app-page-ten>
    </ion-slide>
  </ion-slides>
  <ion-button (click)="swipeNext()" expand="block">次のスライド</ion-button>
  <ion-button (click)="swipePrev()" expand="block">前のスライド</ion-button>
</ion-content>

*ngIf="isVisibleSlide[${スライドのインデックス}]"を追加することで、フラグの配列によるスライドの表示・非表示の機能を追加しました。

home.page.ts

import { Component, ViewChild, OnInit } from '@angular/core';
import { IonSlides } from '@ionic/angular';

/**
 * スライドを10枚表示する画面
 *
 * @export
 * @class HomePage
 */
@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  @ViewChild('slides') slides: IonSlides;
  // スライドの表示フラグ
  public isVisibleSlide: boolean[];
  // スライドの枚数
  private readonly numberOfSlides = 10;

  constructor() {}

  public async ngOnInit(): Promise<void> {
    // 全てのスライドを非表示にする
    this.isVisibleSlide = Array(this.numberOfSlides).fill(false);
    this.initShowSlides();
  }

  /**
   * 次のスライドに進む
   *
   * @memberof HomePage
   */
  public swipeNext(): void {
    this.slides.slideNext();
  }

  /**
   * 前のスライドに戻る
   *
   * @memberof HomePage
   */
  public swipePrev(): void {
    this.slides.slidePrev();
  }

  /**
   * スライドを進んだ時に呼び出される
   *
   * @returns {Promise<void>}
   * @memberof HomePage
   */
  public async loadNext(): Promise<void> {
    await this.showNextSlide();
  }

  /**
   * スライドを戻った時に呼び出される
   *
   * @returns {Promise<void>}
   * @memberof HomePage
   */
  public async loadPrev(): Promise<void> {
    await this.showPrevSlide();
  }

  /**
   * 初期処理として1番目と2番目のスライドを表示する
   *
   * @private
   * @memberof HomePage
   */
  private initShowSlides(): void {
    this.isVisibleSlide[0] = true;
    this.isVisibleSlide[1] = true;
  }

  /**
   * 前方向のスライド移動に伴い、スライドの表示・非表示を切り替える。
   *
   * @private
   * @returns {Promise<void>}
   * @memberof HomePage
   */
  private async showNextSlide(): Promise<void> {
    const index = await this.slides.getActiveIndex();
    if (index > 1) {
      this.isVisibleSlide[index - 2] = false;
    }
    if (index < this.numberOfSlides - 1) {
      this.isVisibleSlide[index + 1] = true;
    }
  }

  /**
   * 後ろ方向のスライド移動に伴い、スライドの表示・非表示を切り替える。
   *
   * @private
   * @returns {Promise<void>}
   * @memberof HomePage
   */
  private async showPrevSlide(): Promise<void> {
    const index = await this.slides.getActiveIndex();
    if (index > 0) {
      this.isVisibleSlide[index - 1] = true;
    }
    if (index < this.numberOfSlides - 2) {
      this.isVisibleSlide[index + 2] = false;
    }
  }
}

スライドの初期表示、スライドを移動するタイミングで、フラグ(isVisibleSlide)を更新することで、現在表示しているスライドとその前後だけ表示の対象にする処理を追加しています。

動作確認

上記のコードを動かして、Chrome DevToolで確認してみます。そうすると、前後と現時点で表示されているスライドしか表示しないことにより、生成するDOMの数を減らし、パフォーマンスを改善できていることがわかります。

f:id:l08084:20210424184720p:plain
4枚目のスライドを表示している時は、3, 4, 5枚目のスライドしか表示されない

上記画像の例では、4枚目のスライドとその前後の3, 5枚目のスライドのみが表示されているため、パフォーマンスへの負荷が減少しています。

案2: Lazy Loading(遅延読み込み)

テンプレートファイル(home.page.html)の内容は、「案1: バーチャルスライド」の時と全く同じ内容でOKです。

続いて、スライド画面のコンポーネントクラスについては、下記のようにスライドの表示フラグを徐々にtrueにしていくメソッドを使用して、擬似的にスライド画面のLazy Loadingを実現しています。

home.page.ts

import { Component, ViewChild, OnInit } from '@angular/core';
import { IonSlides } from '@ionic/angular';
import { interval, Subscription } from 'rxjs';

/**
 * スライドを10枚表示する画面
 *
 * @export
 * @class HomePage
 */
@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  @ViewChild('slides') slides: IonSlides;
  // スライドの表示フラグ
  public isVisibleSlide: boolean[];
  // スライドの枚数
  private readonly numberOfSlides = 10;
  private visibleSlidesCallbackInterbal: Subscription;

  constructor() {}

  public async ngOnInit(): Promise<void> {
    // 全てのスライドを非表示にする
    this.isVisibleSlide = Array(this.numberOfSlides).fill(false);
    this.lazyLodingSlides();
  }

  public ngOnDestroy(): void {
    this.visibleSlidesCallbackInterbal.unsubscribe();
  }

  /**
   * 次のスライドに進む
   *
   * @memberof HomePage
   */
  public swipeNext(): void {
    this.slides.slideNext();
  }

  /**
   * 前のスライドに戻る
   *
   * @memberof HomePage
   */
  public swipePrev(): void {
    this.slides.slidePrev();
  }

  /**
   * スライドを徐々に表示していく
   *
   * @private
   * @memberof HomePage
   */
  private lazyLodingSlides(): void {
    this.isVisibleSlide[0] = true;
    this.isVisibleSlide[1] = true;
    let counter = 2;
    this.visibleSlidesCallbackInterbal = interval(500).subscribe(() => {
      if (counter === this.numberOfSlides) {
        this.visibleSlidesCallbackInterbal.unsubscribe();
        return;
      }
      this.isVisibleSlide[counter++] = true;
    });
  }
}

おわりに

パフォーマンス改善の効果がより高いのは、「案1: バーチャルスライド」の方だと思うのですが、スライドを消したり表示したりする仕様上、デグレーションによるバグが発生しやすいように思えます。

その点、「案2: Lazy Loading(遅延読み込み)」についてはデグレーションによるバグが発生する可能性が案1よりも少なくすみそうです。

参考サイト

Ion-Slides: Mobile Touch Slider with Built-In & Custom Animation

ionic framework - How to slide manually to the next slide in ion-slide - Stack Overflow

Angular 7正式版リリース。バーチャルスクロール、ドラッグ&ドロップのサポートなど、6カ月ぶりのメジャーバージョンアップ - Publickey

【Angular】ngIf と hidden の個人的な使い分け - 開発覚書はてな版

Angular Material UI component library

RxJS