HarmonyOS Nextのカスタムコンポーネント凍結機能の解説

一、機能の概要

HarmonyOS Nextのカスタムコンポーネント凍結機能は、複雑なUIページのパフォーマンスを最適化するために設計されています。多ページスタック、長いリスト、またはグリッドレイアウトなどのシナリオで特に効果的です。状態変数が複数のUIコンポーネントにバインドされている場合、その変化は大量のUIコンポーネントのリフレッシュを引き起こし、インターフェースのカクつきや応答遅延を招く可能性があります。この機能は、freezeWhenInactive属性を設定することでコンポーネント凍結メカニズムを有効にし、アクティブ状態のカスタムコンポーネントのみを更新することで更新範囲を縮小し、複雑なUIシナリオにおけるリフレッシュ効率を向上させます。非アクティブ状態のコンポーネントが再びアクティブになると、状態管理フレームワークはUIが正しく表示されるように必要なリフレッシュ操作を実行し、多ページスタック、長いリスト、またはグリッドレイアウトなどで現在表示されているコンポーネントのみをリフレッシュし、不可視コンポーネントのリフレッシュを遅延させる、必要に応じたリフレッシュを実現します。

適用シナリオ

  1. ページルーティング:現在のスタックトップページがactiveで、非表示の非スタックトップページがinactiveです。
  2. TabContent:現在表示されているTabContent内のカスタムコンポーネントはactive状態にあり、他はinactiveです(初回レンダリング時、Tabは現在表示されているTabContentのみを作成し、すべてのTabContentを切り替えた後で初めてすべてを作成します)。
  3. LazyForEach:現在表示されているLazyForEach内のカスタムコンポーネントのみがactiveで、キャッシュされたノードのコンポーネントはinactiveです。
  4. Navigation:現在表示されているNavDestination内のカスタムコンポーネントはactiveで、他の表示されていないNavDestinationコンポーネントはinactiveです。

注意点

コンポーネントのactive/inactiveは可視性と同一ではありません。例えば、スタックレイアウト(Stack)でマスクされたコンポーネントは不可視ですが、inactive状態とは見なされず、コンポーネント凍結の適用範囲外です。この機能はAPIバージョン11からサポートされています。

二、機能の例

(一)ページルーティング

  1. ページAからページBへ遷移
  • ページAの「first page storageLink + 1」ボタンをクリックすると、storageLink状態変数が変更され、@Watchで登録されたfirstメソッドが呼び出されます。
  • router.pushUrl({url: 'pages/second'})でページBに遷移すると、ページAは非表示になり、状態がinactiveになります。この時、ページBの「this.storageLink2 += 2」ボタンをクリックすると、ページAの状態変数が凍結されているため、ページBで登録された@Watchsecondメソッドのみがコールバックされます。
  • 「back」をクリックしてページAに戻ると、ページAの状態がinactiveからactiveに戻り、以前に凍結された状態変数が再リフレッシュされ、@Watchで登録されたfirstメソッドが再度呼び出されます。
  1. 関連コード例
// ページA
import { router } from '@kit.ArkUI';
@Entry
@Component({ freezeWhenInactive: true })
struct PageA {
  @StorageLink('PropA') @Watch("onCounterChange") counter: number = 47;
  onCounterChange() {
    console.info("ページA: " + `${this.counter}`)
  }
  build() {
    Column() {
      Text(`ページAから: ${this.counter}`).fontSize(50)
      Button('カウントアップ').fontSize(30)
       .onClick(() => {
          this.counter += 1
        })
      Button('次のページへ').fontSize(30)
       .onClick(() => {
          router.pushUrl({ url: 'pages/PageB' })
        })
    }
  }
}

// ページB
import { router } from '@kit.ArkUI';
@Entry
@Component({ freezeWhenInactive: true })
struct PageB {
  @StorageLink('PropA') @Watch("onCounterChangeB") counter: number = 1;
  onCounterChangeB() {
    console.info("ページB: " + `${this.counter}`)
  }
  build() {
    Column() {
      Text(`ページBから: ${this.counter}`).fontSize(50)
      Button('戻る').fontSize(30)
       .onClick(() => {
          router.back()
        })
      Button('カウントアップ x2').fontSize(30)
       .onClick(() => {
          this.counter += 2
        })
    }
  }
}

(二)TabContent

  1. 操作と応答
  • 「change message」をクリックしてmessageの値を変更すると、現在表示されているTabContentコンポーネント内の@Watchで登録されたonDataChangeメソッドがトリガーされます。
  • 別のTabContentに切り替えると、そのTabContentの状態がinactiveからactiveに変わり、対応する@Watchで登録されたonDataChangeメソッドがトリガーされます。
  • 再度「change message」をクリックしてmessageの値を変更すると、現在表示されているTabContentの子コンポーネント内の@Watchで登録されたonDataChangeメソッドのみがトリガーされます。
  1. 関連コード例
@Entry
@Component
struct TabDemo {
  @State @Watch("onDataChange") data: number = 0;
  private tabs: number[] = [0, 1]
  onDataChange() {
    console.info(`TabContent データ変更コールバック: ${this.data}`)
  }
  build() {
    Row() {
      Column() {
        Button('データ変更').onClick(() => {
          this.data++
        })
        Tabs() {
          ForEach(this.tabs, (item: number ) => {
            TabContent() {
              TabItemComponent({ data: this.data, index: item })
            }.tabBar(`タブ${item}`)
          }, (item: number) => item.toString())
        }
      }
     .width('100%')
    }
   .height('100%')
  }
}

@Component({ freezeWhenInactive: true })
struct TabItemComponent {
  @Link @Watch("onDataChange") data: number
  private index: number = 0
  onDataChange() {
    console.info(`子コンポーネント データ変更コールバック: ${this.data}, インデックス: ${this.index}`)
  }
  build() {
    Text(`データ: ${this.data}, インデックス: ${this.index}`)
     .fontSize(50)
     .fontWeight(FontWeight.Bold)
  }
}

(三)LazyForEach

  1. 操作と応答
  • 「change message」をクリックしてmessageの値を変更すると、現在表示されているListItem内の子コンポーネントの@Watchで登録されたonValueChangeメソッドがトリガーされます。キャッシュされたノードの@Watchで登録されたメソッドはトリガーされません(コンポーネント凍結を使用しない場合、現在表示されているListItemとcachecountでキャッシュされたノードの@Watchで登録されたonValueChangeメソッドが両方ともwatchコールバックをトリガーします)。
  • List領域外のListItemがList領域内にスライドすると、状態がinactiveからactiveに変わり、対応する@Watchで登録されたonValueChangeメソッドがトリガーされます。
  • 再度「change message」をクリックしてmessageの値を変更すると、現在表示されているListItem内の子コンポーネントの@Watchで登録されたonValueChangeメソッドのみがトリガーされます。
  1. 関連コード例
// データソース関連クラス
class DataSource implements IDataSource {
  // 省略されたコード
}
class MyDataSource extends DataSource {
  // 省略されたコード
}

@Entry
@Component
struct LazyListDemo {
  private dataSource: MyDataSource = new MyDataSource();
  @State @Watch("onValueChange") value: number = 0;
  onValueChange() {
    console.info(`LazyForEach データ変更コールバック: ${this.value}`)
  }
  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.dataSource.pushData(`アイテム ${i}`)
    }
  }
  build() {
    Column() {
      Button('データ変更').onClick(() => {
        this.value++
      })
      List({ space: 3 }) {
        LazyForEach(this.dataSource, (item: string ) => {
          ListItem() {
            ListItemComponent({ value: this.value, index: item })
          }
        }, (item: string) => item)
      }.cachedCount(5).height(500)
    }
  }
}

@Component({ freezeWhenInactive: true })
struct ListItemComponent {
  @Link @Watch("onValueChange") value: number;
  private index: string = "";
  aboutToAppear() {
    console.info(`ListItemComponent aboutToAppear インデックス: ${this.index}`)
  }
  onValueChange() {
    console.info(`ListItemComponent データ変更コールバック: ${this.value}, インデックス: ${this.index}`)
  }
  build() {
    Text(`データ: ${this.value}, インデックス: ${this.index}`)
     .width('90%')
     .height(160)
     .backgroundColor(0xAFEEEE)
     .textAlign(TextAlign.Center)
     .fontSize(30)
     .fontWeight(FontWeight.Bold)
  }
}

(四)Navigation

  1. 操作と応答
  • 「change message」をクリックしてmessageの値を変更すると、現在表示されているMyNavigationTestStackコンポーネント内の@Watchで登録されたinfoメソッドがトリガーされます。
  • 「Next Page」をクリックしてPageOneに切り替え、pageOneStackノードを作成します。再度「change message」をクリックすると、pageOneStack内のNavigationContentMsgStack子コンポーネントの@Watchで登録されたinfoメソッドのみがトリガーされます。
  • このように、ページ切り替えの過程で、現在表示されているページに関連するコンポーネントのみが状態変数の変化に応答します。 「Back Page」をクリックして戻ると、対応するページのコンポーネントが再び状態変化に応答します。
  1. 関連コード例
@Entry
@Component
struct NavigationDemo {
  @Provide('pageInfo') navStack: NavPathStack = new NavPathStack();
  @State @Watch("onStatusChange") status: number = 0;
  @State logCount: number = 0;
  onStatusChange() {
    console.info(`Navigation データ変更コールバック: ${this.status}`);
  }
  @Builder
  PageBuilder(name: string) {
    if (name === 'pageOne') {
      NavPageOne({ status: this.status, logCount: this.logCount })
    } else if (name === 'pageTwo') {
      NavPageTwo({ status: this.status, logCount: this.logCount })
    } else if (name === 'pageThree') {
      NavPageThree({ status: this.status, logCount: this.logCount })
    }
  }
  build() {
    Column() {
      Button('ステータス変更')
       .onClick(() => {
          this.status++;
        })
      Navigation(this.navStack) {
        Column() {
          Button('次のページへ', { stateEffect: true, type: ButtonType.Capsule })
           .width('80%')
           .height(40)
           .margin(20)
           .onClick(() => {
              this.navStack.pushPath({ name: 'pageOne' });
            })
        }
      }.title('ナビゲーション')
     .navDestination(this.PageBuilder)
     .mode(NavigationMode.Stack)
    }
  }
}

@Component
struct NavPageOne {
  // 省略されたコード
  build() {
    NavDestination() {
      Column() {
        NavPageContent({ status: this.status, index: this.index, logCount: this.logCount })
        Text("現在のスタックサイズ: " + `${this.navStack.size()}`)
         .fontSize(30)
         .fontWeight(FontWeight.Bold)
        Button('次のページへ', { stateEffect: true, type: ButtonType.Capsule })
         .width('80%')
         .height(40)
         .margin(20)
         .onClick(() => {
            this.navStack.pushPathByName('pageTwo', null);
          })
        Button('前のページへ', { stateEffect: true, type: ButtonType.Capsule })
         .width('80%')
         .height(40)
         .margin(20)
         .onClick(() => {
            this.navStack.pop();
          })
      }.width('100%').height('100%')
    }.title('ページ1')
   .onBackPressed(() => {
      this.navStack.pop();
      return true;
    })
  }
}

// NavPageTwoとNavPageThreeの構造は同様で、省略されています。

@Component({ freezeWhenInactive: true })
struct NavPageContent {
  @Link @Watch("onStatusChange") status: number;
  @Link index: number;
  @Link logCount: number;
  onStatusChange() {
    console.info(`NavigationContent データ変更コールバック: ${this.status}`);
    console.info(`NavigationContent ---- コンテンツ ${this.index} によって呼び出されました`);
    this.logCount++;
  }
  build() {
    Column() {
      Text(`ステータス: ${this.status}`)
       .fontSize(30)
       .fontWeight(FontWeight.Bold)
      Text(`ログカウント: ${this.logCount}`)
       .fontSize(30)
       .fontWeight(FontWeight.Bold)
    }
  }
}

タグ: HarmonyOS Next カスタムコンポーネント パフォーマンス最適化 UI 凍結機能

6月11日 19:43 投稿