HarmonyOS API14開発:Gridレイアウトのパフォーマンス最適化によるカクつきとフレームドロップの軽減

概要

大規模で複雑なアプリケーションを構築する際、パフォーマンス最適化は非常に重要です。Gridレイアウトは、ページの均等分割能力、子コンポーネントの占比制御能力、およびアダプティブレイアウト能力を向上させる効率的なレイアウト方法です。本記事では、Gridの高性能開発における応用について紹介します。具体的には、遅延読み込み、cachedCount、コンポーネントの再利用、およびGridLayoutOptionsを使用したGridItemサイズ設定などの方法を含め、開発者がGridレイアウトのパフォーマンスを最適化し、読み込みとレンダリング時間を短縮し、ユーザーエクスペリエンスを向上させる方法を説明します。

遅延読み込みなどの方法でGridパフォーマンスを向上させる

使用シナリオ

遅延読み込み:LazyForEachは、大量のデータを処理する際にパフォーマンスを向上させる最適化戦略です。データを必要に応じて読み込む機能を提供し、一度に大量のデータを読み込む際の長い待ち時間や過剰なリソース使用の問題を解決し、ページの応答速度を向上させます。通常、List、Grid、およびSwiperなどのコンテナコンポーネントで使用されます。Gridが同じレイアウトを持つ子コンポーネントを繰り返しレンダリングする必要があり、大量のデータを表示する必要がある場合、すべてのデータを一度に読み込むことでパフォーマンスのボトルネックが発生する可能性があるため、LazyForEach遅延読み込みを検討する必要があります。

cachedCount:Gridコンポーネントでは、cachedCountは事前読み込みするGridItemの数量を設定するために使用されます。LazyForEach遅延読み込みを使用する場合にのみ有効です。Gridが遅延読み込みデータ項目に比較的時間がかかる場合、たとえばネットワークからビデオデータや画像を取得する必要がある場合、スライド中に白いブロックが表示される問題が発生しやすい場合、cachedCountを使用してGridItemのキャッシュ数量を調整することを検討する必要があります。

コンポーネントの再利用:コンポーネントの再利用は、再利用可能なコンポーネントオブジェクトのキャッシュリソースプールを提供し、すでに作成されキャッシュされたコンポーネントオブジェクトを再利用することで、コンポーネントの短時間での頻繁な作成と破棄のオーバーヘッドを削減し、コンポーネントのレンダリング効率を向上させます。開発者のアプリケーションで、スライドシーンで同じタイプのカスタムコンポーネントのインスタンスを頻繁に作成および破棄し、条件レンダリングの制御ブランチを繰り返し切り替えるシナリオがあり、制御ブランチ内のコンポーネントサブツリー構造が比較的複雑で、UIスレッドのフレームレートのボトルネックになっている場合、ページの読み込み速度と応答速度を向上させるためにコンポーネントの再利用を使用する必要があります。

シナリオの例

以下は、遅延読み込み、cachedCount、コンポーネントの再利用に関するGridコンポーネントの基本的な例です:

// MyDataSourceクラスはIDataSourceインターフェースを実装
class MyDataSource implements IDataSource {
  private dataArray: number[] = [];

  public addData(data: number): void {
    this.dataArray.push(data);
  }

  // データソースのデータ総数
  public getTotalCount(): number {
    return this.dataArray.length;
  }

  // 指定されたインデックス位置のデータを返す
  public getItem(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct MyComponent {
  // データソース
  private dataSource: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 1; i < 1000; i++) {
      this.dataSource.addData(i);
    }
  }

  build() {
    Column({ space: 5 }) {
      Grid() {
        LazyForEach(this.dataSource, (item: number) => {
          GridItem() {
            // 再利用可能なカスタムコンポーネントを使用
            ReusableChildComponent({ value: item })
          }
        }, (item: string) => item.toString())
      }
      .cachedCount(2) // GridItemのキャッシュ数量を設定
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .margin(10)
      .height(500)
      .backgroundColor(0xFAEEE0)
    }
  }
}

// カスタムコンポーネントは@Reusableデコレータで修飾され、コンポーネントの再利用能力を持つことを示します
@Reusable
@Component
struct ReusableChildComponent {
  @State value: number = 0;

  // aboutToReuseは再利用キャッシュからコンポーネントツリーに追加される前に呼び出され、コンポーネントの状態変数を更新して正しいコンテンツを表示するために使用できます
  // aboutToReuseのパラメータタイプはanyをサポートしなくなりました。ここではRecordを使用して明確なデータタイプを指定します。Recordは、プロパティキーがKeysで、プロパティ値がTypeであるオブジェクトタイプを構築するために使用されます
  aboutToReuse(params: Record<string, number>) {
    this.value = params.value;
  }

  build() {
    Column() {
      Image($r('app.media.icon'))
        .objectFit(ImageFit.Fill)
        .layoutWeight(1)
      Text(`画像${this.value}`)
        .fontSize(16)
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height(120)
    .backgroundColor(0xF9CF93)
  }
}
  • 遅延読み込みを使用することで、Gridの読み込みとレンダリング時間を効果的に短縮できます。また、大量のGridItemを処理する場合、遅延読み込みはメモリとCPUリソースの消費を大幅に節約できます。例ではLazyForEachを使用してデータの遅延読み込みを行い、Gridレイアウトは表示領域に応じてGridItemコンポーネントを作成し、GridItemが表示領域外にスライドアウトしたときに破棄してメモリ使用量を削減します。
  • 遅延読み込み方式でGridをレンダリングする場合、cachedCountを適切に使用することで、アプリケーションにより良いスクロール体験を提供し、スライド中に表示される白いブロックを減らすことができます。例ではGridがcachedCountプロパティを使用してGridItemのキャッシュ数量を設定し、Gridは表示領域の前後にcachedCount×列数個のGridItemをキャッシュし、表示およびキャッシュ範囲を超えるGridItemは解放されます。cachedCountの増加はCPU、メモリのオーバーヘッドを増大させます。使用する際は、実際の状況に応じてパフォーマンスとユーザーエクスペリエンスを総合的に調整する必要があります。
  • コンポーネントの再利用は、Gridのスクロールシーンや条件レンダリングの繰り返し切り替えシーンにおいて、GridItemの頻繁な作成と破棄のパフォーマンスをさらに最適化し、ページの読み込み速度と応答速度を向上させます。例では@Reusableデコレータを使用してGridItem内のカスタムコンポーネントReusableChildComponentを修飾し、そのコンポーネントが再利用能力を持つことを示しています。同時に、カスタムコンポーネントのパラメータをライフサイクルコールバック関数aboutToReuseに渡す必要があります。aboutToReuseは、Gridがスライド時に再利用キャッシュからコンポーネントツリーに追加される前にトリガーされ、コンポーネントの状態変数を更新して正しいコンテンツを表示するために使用されます。@Link、@StorageLink、@ObjectLink、@Consumeなどの自動更新値の状態変数をaboutToReuseで更新する必要はないことに注意してください。これは不要なコンポーネントの再レンダリングをトリガーする可能性があります。

GridLayoutOptionsを使用してGridパフォーマンスを向上させる

GridLayoutOptionsレイアウトオプションは、rowsTemplate、columnsTemplateのいずれか一つのみを設定するGridで使用され、columnStart/columnEndを使用してGridItemが複数列を占有するように制御し、rowStart/rowEndを使用してGridItemが複数行を占有するように制御するシナリオを置き換えることができます。

使用シナリオ

削除またはドラッグなどによるGridItem位置の変更:Gridに大量のGridItemが存在し、columnStart/columnEnd、rowStart/rowEndを使用してGridItemサイズを設定すると、このシナリオではGridのレンダリングに時間がかかりすぎる場合、GridLayoutOptionsを使用してパフォーマンスを向上させることを検討する必要があります。columnStart/columnEnd、rowStart/rowEndを使用すると、再レンダリング時にすべてのGridItemノードが再構築されます。一方、GridLayoutOptionsを使用すると、再構築は不要で、レンダリングと読み込み時間が短縮されます。

scrollToIndexを使用して指定GridItemにスライドする:Gridに大量のGridItemが存在し、columnStart/columnEnd、rowStart/rowEndを使用してGridItemサイズを設定すると、このシナリオでは時間がかかりすぎる場合、GridLayoutOptionsを使用してパフォーマンスを向上させることを検討する必要があります。columnStart/columnEnd、rowStart/rowEndレイアウトを使用すると、scrollToIndexで指定Indexにスライドする場合、GridはGridItemを順次走査して位置を検索します。一方、GridLayoutOptionsレイアウトを使用すると、scrollToIndexで指定Indexにスライドする場合、計算方式で位置を検索するため、GridItem位置の検索効率がより高くなります。

シナリオの例

以下では、GridでscrollToIndexを使用して指定位置にスライドするシナリオを紹介します。他のシナリオについては説明しません。

悪い例:

columnStart、columnEndを使用してGridItemサイズを設定します。

// パフォーマンス計測モジュールをインポート
import hiTraceMeter from '@ohos.hiTraceMeter';

@Component
struct TextItem {
  @State content: string = "";

  build() {
    Text(this.content)
      .fontSize(16)
      .backgroundColor(0xF9CF93)
      .width('100%')
      .height(80)
      .textAlign(TextAlign.Center)
  }

  aboutToAppear() {
    // 計測タスクを終了
    hiTraceMeter.finishTrace("useColumnStartColumnEnd", 1);
  }
}

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];

  public addData(data: string): void {
    this.dataArray.push(data);
  }

  public getTotalCount(): number {
    return this.dataArray.length;
  }

  public getItem(index: number): string {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct GridExample {
  private dataSource: MyDataSource = new MyDataSource();
  scroller: Scroller = new Scroller();

  aboutToAppear() {
    for (let i = 1; i <= 2000; i++) {
      this.dataSource.addData(i + '');
    }
  }

  build() {
    Column({ space: 5 }) {
      Text('columnStart、columnEndを使用してGridItemサイズを設定').fontColor(0xCCCCCC).fontSize(9).width('90%')
      Grid(this.scroller) {
        LazyForEach(this.dataSource, (item: string, index: number) => {
          if ((index % 4) === 0) {
            GridItem() {
              TextItem({ content: item })
            }
            .columnStart(0).columnEnd(2)
          } else {
            GridItem() {
              TextItem({ content: item })
            }
          }
        }, (item: string) => item)
      }
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .width('90%')
      .height('40%')

      Button("scrollToIndex:1900").onClick(() => {
        // 計測タスクを開始
        hiTraceMeter.startTrace("useColumnStartColumnEnd", 1);
        this.scroller.scrollToIndex(1900);
      })
    }.width('100%')
    .margin({ top: 5 })
  }
}

良い例:

GridLayoutOptionsを使用してGridItemサイズを設定し、レイアウト効果は悪い例と一致します。

// パフォーマンス計測モジュールをインポート
import hiTraceMeter from '@ohos.hiTraceMeter';

@Component
struct TextItem {
  @State content: string = "";

  build() {
    Text(this.content)
      .fontSize(16)
      .backgroundColor(0xF9CF93)
      .width('100%')
      .height(80)
      .textAlign(TextAlign.Center)
  }

  aboutToAppear() {
    // 計測タスクを終了
    hiTraceMeter.finishTrace("useGridLayoutOptions", 1);
  }
}

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];

  public addData(data: string): void {
    this.dataArray.push(data);
  }

  public getTotalCount(): number {
    return this.dataArray.length;
  }

  public getItem(index: number): string {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct GridExample {
  private dataSource: MyDataSource = new MyDataSource();
  scroller: Scroller = new Scroller();
  private specialIndexes: number[] = [];
  layoutOptions: GridLayoutOptions = {
    regularSize: [1, 1],
    specialIndexes: this.specialIndexes,
  };

  aboutToAppear() {
    for (let i = 1; i <= 2000; i++) {
      this.dataSource.addData(i + '');
      if ((i - 1) % 4 === 0) {
        this.specialIndexes.push(i - 1);
      }
    }
  }

  build() {
    Column({ space: 5 }) {
      Text('GridLayoutOptionsを使用してGridItemサイズを設定').fontColor(0xCCCCCC).fontSize(9).width('90%')
      Grid(this.scroller, this.layoutOptions) {
        LazyForEach(this.dataSource, (item: string, index: number) => {
          GridItem() {
            TextItem({ content: item })
          }
        }, (item: string) => item)
      }
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .width('90%')
      .height('40%')

      Button("scrollToIndex:1900").onClick(() => {
        // 計測タスクを開始
        hiTraceMeter.startTrace("useGridLayoutOptions", 1);
        this.scroller.scrollToIndex(1900);
      })
    }.width('100%')
    .margin({ top: 5 })
  }
}

効果の比較

良い例と悪い例の同じ操作手順:ボタンをクリックして、scrollToIndexを呼び出す前にstartTraceを使用してパフォーマンス計測を開始します。Gridが指定GridItem位置を見つけ、GridItemノードのレンダリングを準備する前に、GridItemカスタムコンポーネントのライフサイクルコールバックaboutToAppearに入り、aboutToAppearでfinishTraceを使用してパフォーマンス計測を停止します。この方法により、startTraceを使用してscrollToIndexの呼び出しを開始計測位置としてマークし、finishTraceを使用して指定位置を見つけた後、最初のGridItemノードのレンダリングを準備する位置を終了計測位置としてマークし、良い例と悪い例のシナリオでの時間データを比較します。

以下では、SmartPerfツールを使用してtraceをキャプチャし、良い例と悪い例のシナリオのパフォーマンス差異を分析します。

図1に示すように、columnStart、columnEndを使用してGridItemサイズを設定するレイアウト方式では、カスタム計測タグ「H:useColumnStartColumnEndGrid」から、scrollToIndexを呼び出して指定Indexを見つけてGridItemノードを構築する準備をするまでの時間が2974msかかっていることがわかります。

図1 columnStart、columnEndを使用した計測情報

図2に示すように、「H:useColumnStartColumnEndGrid」計測タグの時間帯には多くの「H:Builder:BuildLazyItem」タグが存在し、Gridが指定Index:1900を見つける際にGridItemノードを順次走査していることがわかります。

図2 columnStart、columnEndを使用した拡大traceタグ情報

図3に示すように、GridLayoutOptionsを使用してGridItemサイズを設定するレイアウト方式では、カスタム計測タグ「H:useGridLayoutOptions」から、scrollToIndexを呼び出して指定Indexを見つけてGridItemノードを構築する準備をするまでの時間が464msかかっていることがわかります。

図3 GridLayoutOptionsを使用した計測情報

図4に示すように、「H:useGridLayoutOptions」計測タグの時間帯には一つの「H:Builder:BuildLazyItem」タグしか存在せず、Gridが指定Index:1900を見つける際に直接一度で指定Indexを見つけていることがわかります。

図4 GridLayoutOptionsを使用した拡大traceタグ情報

上記の分析から、同じレイアウトの場合、columnStart、columnEndを使用してGridItemサイズを設定する方式では、GridがscrollToIndexを使用して指定Indexを見つける際にGridItemノードを順次走査し、検索プロセスに時間がかかることがわかります。一方、GridLayoutOptionsを使用してGridItemサイズを設定する方式では、計算によって直接指定Indexを見つけるため、検索プロセスの時間が短縮されます。したがって、GridLayoutOptionsを使用してGridItemサイズを設定する方式は、scrollToIndexを使用して指定Indexにスライドする際に、Gridの読み込み時間を効果的に短縮し、アプリケーションのパフォーマンスを向上させることができます。

タグ: HarmonyOS Grid パフォーマンス最適化 遅延読み込み コンポーネント再利用

7月14日 17:26 投稿