Performance e experiência do usuário no Ionic: carregamento, feedback e otimização

Capítulo 18

Tempo estimado de leitura: 11 minutos

+ Exercício

O que “performance” significa no Ionic (e por que o usuário percebe)

Em apps Ionic, performance não é apenas “rodar rápido”: é manter a interface responsiva, com transições suaves, rolagem sem travar e feedback imediato quando algo demora (rede, processamento, leitura de disco). A experiência do usuário melhora quando você combina três frentes: carregamento bem comunicado (skeleton/spinner), estados claros (vazio/erro) e otimizações (renderização, listas, imagens e cache).

Sintomas comuns de gargalo

  • Lista “engasga” ao rolar (muitos itens renderizados ou imagens pesadas).
  • Tela “pisca” ao atualizar dados (re-renderizações desnecessárias).
  • Botão parece não responder (falta de feedback imediato).
  • Tempo de carregamento longo sem indicação (usuário acha que travou).

Feedback de carregamento: skeleton, spinners e bloqueio consciente

Quando usar skeleton vs spinner

  • Skeleton: quando você sabe o formato do conteúdo (lista, cards, perfil). Mantém a percepção de velocidade e evita “layout shift”.
  • Spinner: para ações curtas e pontuais (enviar formulário, salvar, autenticar). Idealmente acompanhado de texto curto.
  • Bloquear interação: apenas quando necessário (ex.: pagamento, operação crítica). Caso contrário, prefira feedback sem bloquear.

Passo a passo: skeleton em lista com ion-skeleton-text

Exemplo de tela que carrega uma lista. Enquanto loading for true, mostramos skeleton; quando terminar, mostramos os itens.

<ion-content>  <ng-container *ngIf="loading; else loaded">    <ion-list>      <ion-item *ngFor="let i of skeletonItems">        <ion-thumbnail slot="start">          <ion-skeleton-text animated="true" style="width: 64px; height: 64px;"></ion-skeleton-text>        </ion-thumbnail>        <ion-label>          <h3><ion-skeleton-text animated="true" style="width: 60%;"></ion-skeleton-text></h3>          <p><ion-skeleton-text animated="true" style="width: 90%;"></ion-skeleton-text></p>        </ion-label>      </ion-item>    </ion-list>  </ng-container>  <ng-template #loaded>    <ion-list>      <ion-item *ngFor="let item of items; trackBy: trackById">        <ion-thumbnail slot="start">          <img [src]="item.thumbUrl" loading="lazy" />        </ion-thumbnail>        <ion-label>          <h3>{{ item.title }}</h3>          <p>{{ item.subtitle }}</p>        </ion-label>      </ion-item>    </ion-list>  </ng-template></ion-content>
export class ListaPage {  loading = true;  items: Array<{ id: string; title: string; subtitle: string; thumbUrl: string }> = [];  skeletonItems = Array.from({ length: 8 });  async ionViewDidEnter() {    this.loading = true;    try {      // Simule a busca real (API/Storage)      const data = await this.fetchItems();      this.items = data;    } finally {      this.loading = false;    }  }  trackById(_: number, item: { id: string }) {    return item.id;  }  private fetchItems(): Promise<any[]> {    return new Promise(resolve => setTimeout(() => resolve([      { id: '1', title: 'Item 1', subtitle: 'Detalhe', thumbUrl: 'assets/img/thumb1.jpg' },      { id: '2', title: 'Item 2', subtitle: 'Detalhe', thumbUrl: 'assets/img/thumb2.jpg' }    ]), 900));  }}

Pontos importantes: trackBy reduz re-renderizações na lista; loading="lazy" ajuda a não carregar todas as imagens de uma vez.

Passo a passo: spinner com IonLoadingController (operações curtas)

Use um loading “modal” quando a ação precisa de confirmação visual imediata e não deve ser repetida.

import { LoadingController } from '@ionic/angular';export class PerfilPage {  constructor(private loadingCtrl: LoadingController) {}  async salvar() {    const loading = await this.loadingCtrl.create({      message: 'Salvando...',      spinner: 'crescent',      backdropDismiss: false    });    await loading.present();    try {      await this.doSave();    } catch (e) {      // trate erro (ver seção de mensagens amigáveis)    } finally {      await loading.dismiss();    }  }  private doSave() {    return new Promise(resolve => setTimeout(resolve, 800));  }}

Estados vazios: quando “não há dados” também é um estado

Um estado vazio bem feito evita confusão e reduz suporte. Ele deve responder: o que aconteceu, por que está vazio (quando possível) e o que o usuário pode fazer agora.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Padrão prático de template: carregando, vazio, erro e sucesso

<ion-content>  <ng-container *ngIf="loading">    <ion-list>      <ion-item *ngFor="let i of skeletonItems">        <ion-label>          <ion-skeleton-text animated="true" style="width: 70%"></ion-skeleton-text>          <ion-skeleton-text animated="true" style="width: 40%"></ion-skeleton-text>        </ion-label>      </ion-item>    </ion-list>  </ng-container>  <ng-container *ngIf="!loading && error">    <div class="state">      <h3>Não foi possível carregar</h3>      <p>Verifique sua conexão e tente novamente.</p>      <ion-button (click)="reload()" expand="block">Tentar novamente</ion-button>    </div>  </ng-container>  <ng-container *ngIf="!loading && !error && items.length === 0">    <div class="state">      <h3>Nada por aqui</h3>      <p>Você ainda não adicionou itens.</p>      <ion-button routerLink="/novo" expand="block">Adicionar</ion-button>    </div>  </ng-container>  <ng-container *ngIf="!loading && !error && items.length > 0">    <ion-list>      <ion-item *ngFor="let item of items; trackBy: trackById">        <ion-label>{{ item.title }}</ion-label>      </ion-item>    </ion-list>  </ng-container></ion-content>
.state {  padding: 24px;  text-align: center;} .state h3 {  margin-top: 8px;  margin-bottom: 8px;} .state p {  opacity: 0.8;  margin-bottom: 16px;}

Mensagens de erro amigáveis: úteis, curtas e acionáveis

Erros “amigáveis” não expõem detalhes técnicos ao usuário final. Em vez disso, informam o impacto e a ação possível. Uma boa prática é mapear erros comuns para mensagens padronizadas.

Passo a passo: mapeando erros para mensagens

type UiError = { title: string; message: string; canRetry?: boolean };function mapToUiError(err: any): UiError {  // Exemplos: adapte ao seu cenário  if (err?.status === 0) {    return { title: 'Sem conexão', message: 'Conecte-se à internet e tente novamente.', canRetry: true };  }  if (err?.status === 404) {    return { title: 'Não encontrado', message: 'O conteúdo não está mais disponível.', canRetry: false };  }  if (err?.status >= 500) {    return { title: 'Instabilidade no serviço', message: 'Tente novamente em instantes.', canRetry: true };  }  return { title: 'Algo deu errado', message: 'Tente novamente.', canRetry: true };}

Para exibir, você pode usar um bloco de estado (como acima) ou um ion-toast para erros não bloqueantes. Prefira estado de tela quando o erro impede a página de funcionar.

Otimizações comuns no Ionic (Angular): menos trabalho por frame

1) Reduzir re-renderizações: OnPush e trackBy

Em listas e componentes com dados que mudam com frequência, reduzir detecções de mudança ajuda a manter a UI fluida.

  • Use trackBy em *ngFor para evitar recriar elementos ao atualizar arrays.
  • Considere ChangeDetectionStrategy.OnPush em componentes de apresentação (que recebem @Input e não dependem de estado global mutável).
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';@Component({  selector: 'app-item-card',  template: `<ion-card><ion-card-header>{{ title }}</ion-card-header></ion-card>`,  changeDetection: ChangeDetectionStrategy.OnPush})export class ItemCardComponent {  @Input() title = '';}

Com OnPush, atualize referências (ex.: crie novo array ao alterar) para o Angular perceber mudanças.

2) Listas eficientes: ion-virtual-scroll (quando aplicável) e paginação

Listas longas são um dos maiores vilões. Estratégias típicas:

  • Paginação/infinite scroll: carregue em blocos e evite milhares de itens na tela.
  • Virtualização: renderize apenas o que está visível (dependendo da versão/stack, avalie suporte e limitações).

Passo a passo: infinite scroll com carregamento incremental

<ion-content>  <ion-list>    <ion-item *ngFor="let item of items; trackBy: trackById">      <ion-label>{{ item.title }}</ion-label>    </ion-item>  </ion-list>  <ion-infinite-scroll (ionInfinite)="loadMore($event)" threshold="150px">    <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Carregando..."></ion-infinite-scroll-content>  </ion-infinite-scroll></ion-content>
page = 0;pageSize = 20;items: Array<{ id: string; title: string }> = [];async ionViewDidEnter() {  this.page = 0;  this.items = [];  await this.fetchPage();}async loadMore(ev: any) {  await this.fetchPage();  ev.target.complete();  // Se acabou, desabilite  if (this.items.length >= this.total) {    ev.target.disabled = true;  }}async fetchPage() {  const next = await this.getItems(this.page, this.pageSize);  this.items = [...this.items, ...next.items];  this.total = next.total;  this.page++;}total = 0;private getItems(page: number, size: number): Promise<{ items: any[]; total: number }> {  return new Promise(resolve => {    setTimeout(() => {      const total = 95;      const start = page * size;      const end = Math.min(start + size, total);      const items = Array.from({ length: end - start }).map((_, i) => ({        id: String(start + i + 1),        title: `Item ${start + i + 1}`      }));      resolve({ items, total });    }, 500);  });}

3) Imagens otimizadas: tamanho certo, lazy load e placeholders

Imagens grandes degradam rolagem e tempo de tela. Boas práticas:

  • Sirva imagens no tamanho aproximado do componente (thumb não deve ser 2000px).
  • Comprima e use formatos modernos (WebP/AVIF quando possível).
  • Lazy load com loading="lazy" e defina width/height para reduzir “pulos” de layout.
  • Placeholder: use skeleton/blur enquanto carrega.
<img  [src]="item.thumbUrl"  loading="lazy"  width="64"  height="64"  style="object-fit: cover; border-radius: 8px;" />

4) Cache controlado: rápido sem ficar desatualizado

Cache melhora percepção de velocidade, mas precisa de regras para não mostrar dados antigos por tempo demais. Uma abordagem simples é cache com TTL (tempo de vida):

  • Ao abrir a tela, mostre cache imediatamente (se existir).
  • Em paralelo, busque dados novos.
  • Se a busca falhar, mantenha cache e avise discretamente.

Passo a passo: cache com TTL (exemplo conceitual)

type CacheEntry<T> = { value: T; savedAt: number };const TTL_MS = 60_000; // 1 minutoasync function getWithCache<T>(key: string, fetcher: () => Promise<T>, storage: any): Promise<T> {  const cached: CacheEntry<T> | null = await storage.get(key);  const now = Date.now();  if (cached && (now - cached.savedAt) < TTL_MS) {    return cached.value;  }  const fresh = await fetcher();  await storage.set(key, { value: fresh, savedAt: now } as CacheEntry<T>);  return fresh;}

Use TTL diferente por tipo de dado: catálogo pode ter TTL maior; mensagens/feeds, menor.

Interface fluida em dispositivos modestos: práticas objetivas

Evite trabalho pesado no thread principal

  • Não faça processamento grande dentro de handlers de UI (cliques, scroll).
  • Quebre tarefas longas em partes menores (ex.: processar em lotes com setTimeout curto) para “devolver” o controle à UI.
  • Prefira pré-processar dados no serviço antes de chegar ao template (menos lógica no HTML).

Reduza watchers e bindings complexos

  • Evite chamar funções no template que rodam a cada detecção de mudança (ex.: {{ calculaAlgo(item) }} em listas). Pré-calcule e armazene.
  • Use pipes puros quando fizer sentido (transformações determinísticas).

Cuide de animações e sombras

  • Sombras pesadas e muitos elementos com blur podem custar caro em GPUs fracas.
  • Prefira transições simples e consistentes; evite animar propriedades que forçam layout (como top, left), prefira transform e opacity.

Controle de memória

  • Desinscreva de streams/observables quando a tela sai (quando aplicável) para evitar vazamentos.
  • Evite manter arrays gigantes em memória se o usuário só precisa de uma página por vez.

Como monitorar gargalos pelo comportamento do app (sem ferramentas complexas)

Checklist de observação durante testes

Comportamento observadoCausa provávelAção típica
Rolagem travando em listaMuitos itens/imagens pesadasPaginação/infinite scroll, otimizar imagens, trackBy
Tela piscando ao atualizarRe-renderizaçõesOnPush, evitar recriar objetos, trackBy
Toque sem respostaThread principal ocupadaQuebrar tarefas, adiar processamento, mostrar loading
Carregamento “mudo”Sem feedbackSkeleton/spinner, estados vazios/erro

Instrumentação simples com marcação de tempo

Meça tempos de operações críticas (carregar lista, renderizar após fetch) para identificar onde está a demora.

async carregar() {  console.time('carregar:total');  console.time('carregar:fetch');  const data = await this.fetchItems();  console.timeEnd('carregar:fetch');  console.time('carregar:bind');  this.items = data;  // Se necessário, aguarde o próximo frame para medir percepção de render  requestAnimationFrame(() => {    console.timeEnd('carregar:bind');    console.timeEnd('carregar:total');  });}

Ao testar em um dispositivo mais fraco, compare tempos e observe se o gargalo é rede (fetch) ou UI (bind/renderização).

Agora responda o exercício sobre o conteúdo:

Em um app Ionic, ao carregar uma lista em que você conhece o formato dos itens (como cards com imagem e texto), qual abordagem tende a melhorar a percepção de velocidade e evitar “pulos” no layout?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Skeletons funcionam melhor quando o formato do conteúdo é conhecido, pois mantêm a UI estável e melhoram a percepção de velocidade, reduzindo layout shift. Spinners são mais indicados para ações curtas e pontuais.

Próximo capitúlo

Build Android com Ionic e Capacitor: geração de APK/AAB e assinatura

Arrow Right Icon
Capa do Ebook gratuito Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript
86%

Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.