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.
- 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()oumaxHeight(), 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()emaxWidth()para restringir as variações de tamanho. - Em vez de
width('auto'), usewrapContent()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);
});
}
}
}
- Preferindo
layoutWeightem vez deflexGrow/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.
- 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.
- 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.
- 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.
- 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.