Otimização de Desempenho de Layout em Aplicações ArkTS

Em aplicações nativas desenvolvidas com ArkTS para HarmonyOS, a performance do layout é crucial para uma experiência de usuário fluida. Interfaces complexas ou dinâmicas podem sofrer com lentidão quando os elementos são reorganizados ou atualizados. A seguir, exploraremos técnicas para aprimorar o desempenho de layout.

  1. Minimizando Recálculos de Layout Redundantes

Recálculos de layout, ou "relayouts" (conhecidos como "layout reflows" em outras plataformas), ocorrem quando o sistema precisa recalcular a posição e o tamanho dos elementos da interface. Em ArkTS, diversas situações podem levar a isso, como mudanças dinâmicas de tamanho de componentes, carregamento assíncrono de conteúdo ou o uso de layouts flexíveis com dimensões incertas.

Cenário 1: Alteração Dinâmica de Dimensões de Componentes Filhos

Quando o tamanho de um elemento filho muda, o contêiner pai pode precisar se ajustar, disparando um recálculo.


@Entry
@Component
struct DynamicSizeAdjustment {
   private isEnlarged: boolean = false;

   build() {
       Column() {
           Button("Alternar Tamanho")
               .onClick(() => this.isEnlarged = !this.isEnlarged)
               .padding(10);

           Text("Texto Mutável")
               .fontSize(this.isEnlarged ? 28 : 18)
               .padding(10);
       }
   }
}
 

Neste exemplo, a alteração do fontSize do Text altera suas dimensões, forçando o Column pai a recalcular e redistribuir o espaço.

Cenário 2: Conteúdo Carregado Assincronamente com Dimensões Variáveis

A chegada de conteúdo com tamanho desconhecido após uma operação assíncrona é outra causa comum.


@Entry
@Component
struct AsyncContentUpdate {
   private displayMessage: string = 'Carregando...';

   build() {
       Column() {
           Button("Buscar Conteúdo")
               .onClick(() => this.fetchRemoteContent())
               .padding(10);

           Text(this.displayMessage)
               .padding(10);
       }
   }

   private async fetchRemoteContent() {
       this.displayMessage = await this.simulateNetworkRequest();
   }

   private async simulateNetworkRequest(): Promise<string> {
       return new Promise(resolve => {
           setTimeout(() => {
               resolve("Conteúdo Carregado Dinamicamente com Sucesso.");
           }, 1200);
       });
   }
}
 </string>

Aqui, a atualização do displayMessage após a simulação de uma requisição de rede pode mudar drasticamente o tamanho do Text, provocando um recálculo do layout pai.

Cenário 3: Flex Layout com Componentes de Dimensões Indefinidas

Em um layout flexível, se os filhos têm dimensões automáticas ('auto'), o contêiner pode precisar de múltiplas passadas para ajustar tudo.


@Entry
@Component
struct FlexibleLayoutExample {
   private sections: string[] = ['Seção A', 'Seção B', 'Seção C'];

   build() {
       Flex() {
           this.sections.forEach(item => {
               Text(item).width('auto').margin(8);
           });
       }
   }
}
 

Com width('auto'), o tamanho final de cada Text é determinado pelo seu conteúdo e pela disponibilidade de espaço no Flex, o que pode levar a um processo de layout mais complexo.

Estratégias de Otimização

Para mitigar esses problemas, podemos aplicar as seguintes otimizações:

Otimização Cenário 1: Mudanças Dinâmicas de Dimensões

  • Utilize matchContent() em conjunto com mudanças de fonte para permitir que o componente se ajuste ao conteúdo de forma mais controlada.
  • Incorpore transições ou animações para suavizar as mudanças de tamanho, o que pode mascarar ou reduzir a percepção de recálculos.

@Entry
@Component
struct OptimizedDynamicSize {
   private isEnlarged: boolean = false;

   build() {
       Column() {
           Button("Alternar Tamanho")
               .onClick(() => this.isEnlarged = !this.isEnlarged)
               .padding(10);

           Text("Texto Mutável")
               .matchContent() // Permite que o texto se ajuste ao conteúdo
               .fontSize(this.isEnlarged ? 28 : 18)
               .padding(10)
               .transition({ duration: 250 }); // Suaviza a transição de tamanho
       }
   }
}
 

Otimização Cenário 2: Conteúdo Assíncrono

  • Defina dimensões máximas ou mínimas para o componente que receberá o conteúdo assíncrono, como maxWidth() ou maxHeight(), para limitar o impacto das mudanças de tamanho.
  • Use placeholders ou esqueletos de carregamento com dimensões fixas para manter o layout estável enquanto o conteúdo é carregado.

@Entry
@Component
struct OptimizedAsyncContent {
   private displayMessage: string = '...Carregando'; // Placeholder inicial

   build() {
       Column() {
           Button("Buscar Conteúdo")
               .onClick(() => this.fetchRemoteContent())
               .padding(10);

           Text(this.displayMessage)
               .maxWidth(400) // Limita a largura máxima para estabilizar o layout
               .padding(10);
       }
   }

   private async fetchRemoteContent() {
       this.displayMessage = await this.simulateNetworkRequest();
   }

   private async simulateNetworkRequest(): Promise<string> {
       return new Promise(resolve => {
           setTimeout(() => {
               resolve("Conteúdo Carregado Dinamicamente com Sucesso.");
           }, 1200);
       });
   }
}
 </string>

Otimização Cenário 3: Flex Layout com Dimensões Indefinidas

  • Para itens em layouts flexíveis, considere usar minWidth() e maxWidth() para restringir as variações de tamanho.
  • Em vez de width('auto'), use wrapContent() se a intenção é que o componente se ajuste ao seu próprio conteúdo, mas sempre complemente com limites.

@Entry
@Component
struct OptimizedFlexibleLayout {
   private sections: string[] = ['Seção A', 'Seção B', 'Seção C'];

   build() {
       Flex() {
           this.sections.forEach(item => {
               Text(item)
                   .wrapContent() // Ajusta-se ao conteúdo
                   .minWidth(60)  // Largura mínima
                   .maxWidth(250) // Largura máxima
                   .margin(8);
           });
       }
   }
}
 
  1. Preferindo layoutWeight em vez de flexGrow/flexShrink

As propriedades flexGrow e flexShrink em layouts Flex podem induzir múltiplos recálculos. Se a soma dos tamanhos dos componentes filhos não se encaixa no contêiner, essas propriedades podem disparar um segundo passe de layout para esticar ou encolher os elementos.

Exemplo Inicial (com flexGrow)


@Entry
@Component
struct LegacyFlexDistribution {
 build() {
   Flex({ direction: FlexDirection.Row }) {
     Text("Painel Esquerdo").flexGrow(1)
     Text("Painel Direito").flexGrow(2)
   }
   .height(100)
   .width('100%')
   .border({ width: 1, color: Color.Gray })
 }
}
 

Aqui, flexGrow pode exigir que o motor de layout recalcule o espaço se as dimensões iniciais dos textos não preencherem o Flex.

Exemplo Otimizado (com layoutWeight)

A propriedade layoutWeight, especialmente quando usada em contêineres como Row ou Column, distribui o espaço restante de forma mais eficiente, geralmente em uma única passada de layout, evitando recálculos secundários.


@Entry
@Component
struct OptimizedWeightedLayout {
 build() {
   Row() {
     Text("Painel Esquerdo").layoutWeight(1)
     Text("Painel Direito").layoutWeight(2)
   }
   .height(100)
   .width('100%')
   .border({ width: 1, color: Color.Gray })
 }
}
 

Ao usar layoutWeight, o sistema de layout aloca o espaço disponível proporcionalmente desde o início, simplificando o processo de cálculo e reduzindo a complexidade.

  1. Design de Layout Responsivo

Um layout não responsivo pode resultar em recálculos constantes quando a orientação do dispositivo muda ou quando a aplicação é executada em diferentes tamanhos de tela. Isso pode levar a problemas de desempenho e uma má experiência do usuário.

Exemplo Não Otiimzado


@Entry
@Component
struct FixedLayoutExample {
 build() {
   Column() {
     Text("Título Principal").fontSize(26)
     Row() {
       Text("Item Menu 1")
       Text("Item Menu 2")
       Text("Item Menu 3")
     }
     .width('100%') // Largura fixa pode causar problemas em telas menores
     .justifyContent(FlexAlign.SpaceAround)
     Text("Conteúdo da Página")
   }
   .padding(20)
 }
}
 

Neste cenário, se a tela for pequena, os itens do Row podem se sobrepor ou ficarem apertados, exigindo redimensionamento e recálculos que afetam a performance.

Exemplo Otimizado com Renderização Condicional

Implementar lógica para adaptar o layout com base nas características do dispositivo (como largura da tela) previne recálculos desnecessários e melhora a adaptabilidade.


import window from '@ohos.window';

@Entry
@Component
struct ResponsiveLayout {
 @State isCompactMode: boolean = false;

 onPageShow() {
   this.checkScreenSize();
   window.on('windowSizeChange', this.onWindowSizeChange.bind(this));
 }

 onPageHide() {
   window.off('windowSizeChange', this.onWindowSizeChange.bind(this));
 }

 onWindowSizeChange(size: window.WindowSize) {
   this.isCompactMode = size.width < 700; // Define o ponto de interrupção para modo compacto
 }

 checkScreenSize() {
   window.getLastWindow(context => {
     context.then(win => {
       let rect = win.getWindowProperties().windowRect;
       this.isCompactMode = rect.width < 700;
     });
   });
 }

 build() {
   Column() {
     Text("Título Principal").fontSize(26);

     if (this.isCompactMode) {
       // Layout vertical para telas pequenas
       Column({ space: 10 }) {
         Text("Opção 1").width('90%').padding(5).backgroundColor(0xEEEEEE)
         Text("Opção 2").width('90%').padding(5).backgroundColor(0xEEEEEE)
         Text("Opção 3").width('90%').padding(5).backgroundColor(0xEEEEEE)
       }
       .width('100%')
     } else {
       // Layout horizontal para telas maiores
       Row({ space: 15 }) {
         Text("Opção 1").layoutWeight(1).padding(5).backgroundColor(0xEEEEEE)
         Text("Opção 2").layoutWeight(1).padding(5).backgroundColor(0xEEEEEE)
         Text("Opção 3").layoutWeight(1).padding(5).backgroundColor(0xEEEEEE)
       }
       .width('100%')
       .justifyContent(FlexAlign.Center)
     }
     Text("Conteúdo da Página").margin({ top: 20 })
   }
   .padding(20)
 }
}
 

Com a renderização condicional, o layout se adapta proativamente ao tamanho da tela, reduzindo a necessidade de recálculos frequentes e melhorando a performance e a experiência do usuário.

  1. Carregamento Preguiçoso (Lazy Loading)

Carregar e renderizar todos os componentes de uma vez, especialmente em listas longas ou interfaces complexas, pode degradar significativamente o tempo de inicialização da aplicação e consumir muita memória.

Exemplo sem Lazy Loading


@Entry
@Component
struct EagerLoadingList {
 private dataItems: { id: number; description: string; details: any }[] = this.generateComplexItems(100);

 generateComplexItems(count: number): any[] {
   let items: any[] = [];
   for (let i = 0; i < count; i++) {
     items.push({
       id: i,
       description: `Descrição do Item ${i + 1}`,
       details: new Array(500).fill(null).map((_, idx) => ({ prop: `valor${idx}` })) // Dados complexos
     });
   }
   return items;
 }

 build() {
   Column() {
     // Todos os itens são renderizados de uma vez, mesmo que não estejam visíveis
     ForEach(this.dataItems, (item) => {
       this.renderDetailedItem(item);
     }, item => item.id.toString());
   }
 }

 @Builder
 renderDetailedItem(item: { id: number; description: string; details: any }) {
   Column() {
     Text(`ID: ${item.id}`).fontWeight(FontWeight.Bold);
     Text(item.description);
     // Suponha que 'details' implique mais componentes e lógica
     Divider().strokeWidth(1).color(Color.Gray).margin({ top: 5, bottom: 5 });
   }
   .padding(10)
   .width('100%')
 }
}
 

Nesse código, todos os 100 itens são criados e renderizados no momento da carga inicial, mesmo que apenas alguns estejam visíveis na tela.

Exemplo Otimizado com LazyForEach

A diretiva LazyForEach é ideal para listas onde o número de itens é grande. Ela renderiza componentes apenas quando eles estão prestes a se tornar visíveis, otimizando o uso de recursos.


@Entry
@Component
struct LazyLoadedList {
 private dataItems: { id: number; description: string; details: any }[] = this.generateComplexItems(100);

 generateComplexItems(count: number): any[] {
   let items: any[] = [];
   for (let i = 0; i < count; i++) {
     items.push({
       id: i,
       description: `Descrição do Item ${i + 1}`,
       details: new Array(500).fill(null).map((_, idx) => ({ prop: `valor${idx}` }))
     });
   }
   return items;
 }

 build() {
   Column() {
     // Itens são renderizados apenas quando necessário
     LazyForEach(this.dataItems, (item) => {
       this.renderDetailedItem(item);
     }, item => item.id.toString());
   }
 }

 @Builder
 renderDetailedItem(item: { id: number; description: string; details: any }) {
   Column() {
     Text(`ID: ${item.id}`).fontWeight(FontWeight.Bold);
     Text(item.description);
     Divider().strokeWidth(1).color(Color.Gray).margin({ top: 5, bottom: 5 });
   }
   .padding(10)
   .width('100%')
 }
}
 

Com LazyForEach, a aplicação inicia mais rapidamente, consome menos memória e proporciona uma experiência de rolagem mais suave, pois os componentes são criados e reciclados de forma inteligente.

  1. Otimizando Atualizações de Dados Complexos

Em ArkTS, a atualização de um objeto de estado pode, por padrão, re-renderizar todas as partes da UI que dependem desse objeto. Para objetos grandes, isso pode ser ineficiente se apenas uma pequena porção dos dados foi alterada.

Exemplo Ineficiente


@Entry
@Component
struct InefficientDataUpdate {
 @State userProfile: { id: number, name: string, email: string, settings: any } = this.createProfile();

 createProfile(): any {
   let profile: any = { id: 1, name: 'João Silva', email: 'joao@example.com', settings: {} };
   for (let i = 0; i < 50; i++) {
     profile.settings[`pref${i}`] = `valor${i}`;
   }
   return profile;
 }

 build() {
   Column({ space: 10 }) {
     Text(`Nome: ${this.userProfile.name}`);
     Text(`Email: ${this.userProfile.email}`);
     Text(`Preferência 0: ${this.userProfile.settings['pref0']}`); // Exibindo uma das preferências
     Button("Atualizar Nome").onClick(() => {
       this.userProfile.name = 'João Oliveira'; // Alteração simples, mas pode re-renderizar tudo
     });
   }
   .padding(20)
 }
}
 

Ao mudar userProfile.name, todo o objeto userProfile é considerado "alterado", potencialmente disparando uma re-renderização de todos os componentes que acessam qualquer propriedade dele, mesmo que não precisem.

Exemplo Otimizado com @Observed e @ObjectLink

As anotações @Observed e @ObjectLink (ou @Prop para propriedades unidirecionais) permitem um rastreamento de mudanças mais granular. Com @Observed em um objeto de classe, e @ObjectLink (ou @Prop) em componentes filhos, apenas as partes da UI que realmente utilizam a propriedade alterada são atualizadas.


// Definição da classe observável
@Observed
class UserPreferences {
 pref0: string = 'valor0';
 pref1: string = 'valor1';
 // ... outras 48 preferências
}

@Observed
class UserProfileData {
 id: number = 1;
 name: string = 'João Silva';
 email: string = 'joao@example.com';
 settings: UserPreferences = new UserPreferences();
}

@Component
struct ProfileDisplayComponent {
 @ObjectLink profile: UserProfileData; // Link para o objeto observável

 build() {
   Column({ space: 5 }) {
     Text(`Nome: ${this.profile.name}`);
     Text(`Email: ${this.profile.email}`);
     // Um componente filho pode focar em uma parte específica
     PreferencesDisplay({ prefs: this.profile.settings });
   }
 }
}

@Component
struct PreferencesDisplay {
 @ObjectLink prefs: UserPreferences; // Link para as preferências do usuário

 build() {
   Column() {
     Text(`Preferência 0: ${this.prefs.pref0}`).fontSize(14).fontColor(Color.Gray);
     // ... apenas as preferências relevantes seriam renderizadas
   }
 }
}

@Entry
@Component
struct OptimizedDataUpdate {
 @State activeUserProfile: UserProfileData = new UserProfileData();

 build() {
   Column({ space: 20 }) {
     ProfileDisplayComponent({ profile: this.activeUserProfile });
     Button("Atualizar Nome do Perfil").onClick(() => {
       this.activeUserProfile.name = 'João Santos'; // Apenas o componente de nome será atualizado
     });
     Button("Alterar Pref. 0").onClick(() => {
       this.activeUserProfile.settings.pref0 = 'novo_valor_preferido'; // Apenas o componente de preferências será atualizado
     });
   }
   .padding(20)
 }
}
 

Com essa abordagem, as atualizações são mais precisas, impactando apenas os componentes necessários e resultando em um desempenho de UI superior, especialmente com grandes estruturas de dados.

  1. Gerenciamento de Memória e Prevenção de Vazamentos

A criação e destruição contínua de objetos, como em jogos ou simulações de partículas, pode sobrecarregar o sistema de gerenciamento de memória (garbage collector), levando a pausas no desempenho (stuttering) e potenciais vazamentos de memória.

Exemplo de Consumo de Memória


@Entry
@Component
struct MemoryHogExample {
 private activeParticles: { id: number, x: number, y: number, color: Color }[] = [];
 private intervalId: number | undefined;
 private particleCount: number = 0;

 onPageShow() {
   this.intervalId = setInterval(() => {
     if (this.activeParticles.length < 50) { // Limite para evitar travar a demo
       this.createRandomParticle();
     }
     if (this.activeParticles.length > 20) {
       this.activeParticles.shift(); // Remove o mais antigo
     }
   }, 100);
 }

 onPageHide() {
   if (this.intervalId !== undefined) {
     clearInterval(this.intervalId);
   }
   this.activeParticles = [];
 }

 createRandomParticle() {
   this.particleCount++;
   this.activeParticles.push({
     id: this.particleCount,
     x: Math.random() * 300,
     y: Math.random() * 200,
     color: Color.Red
   });
 }

 build() {
   Column() {
     Text(`Partículas ativas: ${this.activeParticles.length}`)
     Stack() {
       ForEach(this.activeParticles, (p) => {
         Circle({ radius: 5 })
           .fill(p.color)
           .position({ x: p.x, y: p.y });
       }, p => p.id.toString());
     }
     .width(300).height(200).border({ width: 1, color: Color.Black })
   }
 }
}
 

Neste caso, partículas são continuamente criadas e destruídas (removidas do array), forçando o garbage collector a trabalhar, o que pode causar lentidão.

Exemplo Otimizado com Pool de Objetos

O padrão de pool de objetos ("Object Pooling") reutiliza objetos em vez de criá-los e destruí-los repetidamente. Objetos são "emprestados" do pool quando necessários e retornados a ele quando não estão mais em uso, onde podem ser reinicializados.


class Particle {
 id: number = 0;
 x: number = 0;
 y: number = 0;
 color: Color = Color.Red;
 isActive: boolean = false;

 reset(newId: number) {
   this.id = newId;
   this.x = Math.random() * 300;
   this.y = Math.random() * 200;
   this.color = [Color.Red, Color.Green, Color.Blue, Color.Yellow][Math.floor(Math.random() * 4)];
   this.isActive = true;
 }
}

class ParticlePool {
 private pool: Particle[] = [];
 private maxPoolSize: number;
 private currentId: number = 0;

 constructor(size: number) {
   this.maxPoolSize = size;
   for (let i = 0; i < size; i++) {
     this.pool.push(new Particle());
   }
 }

 getParticle(): Particle | undefined {
   for (let i = 0; i < this.pool.length; i++) {
     if (!this.pool[i].isActive) {
       this.currentId++;
       this.pool[i].reset(this.currentId);
       return this.pool[i];
     }
   }
   // Opcional: expandir o pool ou retornar undefined se o pool estiver cheio e não puder expandir
   if (this.pool.length < this.maxPoolSize * 2) { // Exemplo de expansão limitada
       let newParticle = new Particle();
       this.pool.push(newParticle);
       this.currentId++;
       newParticle.reset(this.currentId);
       return newParticle;
   }
   return undefined;
 }

 returnParticle(particle: Particle) {
   particle.isActive = false;
 }

 getAllActiveParticles(): Particle[] {
   return this.pool.filter(p => p.isActive);
 }
}

@Entry
@Component
struct OptimizedMemoryManagement {
 private particleSystem: ParticlePool = new ParticlePool(30); // Pool com 30 partículas iniciais
 private intervalId: number | undefined;

 onPageShow() {
   this.intervalId = setInterval(() => {
     let newParticle = this.particleSystem.getParticle();
     if (newParticle) {
       // Simular um tempo de vida para a partícula
       setTimeout(() => {
         this.particleSystem.returnParticle(newParticle);
         this.update(); // Força a atualização da UI
       }, 3000);
     }
     this.update(); // Força a atualização da UI para mostrar as partículas ativas
   }, 500);
 }

 onPageHide() {
   if (this.intervalId !== undefined) {
     clearInterval(this.intervalId);
   }
 }

 build() {
   Column() {
     Text(`Partículas ativas (pool): ${this.particleSystem.getAllActiveParticles().length}`)
     Stack() {
       ForEach(this.particleSystem.getAllActiveParticles(), (p) => {
         Circle({ radius: 5 })
           .fill(p.color)
           .position({ x: p.x, y: p.y });
       }, p => p.id.toString());
     }
     .width(300).height(200).border({ width: 1, color: Color.Black })
   }
 }
}
 

Com o pool de objetos, a alocação e desalocação de memória são significativamente reduzidas. Os objetos são reutilizados, minimizando a pressão sobre o garbage collector e melhorando a fluidez da aplicação.

Tags: ArkTS HarmonyOS desempenho UI otimização de layout Design Responsivo

Publicado em 9-4 08:07