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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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
trackByem*ngForpara evitar recriar elementos ao atualizar arrays. - Considere
ChangeDetectionStrategy.OnPushem componentes de apresentação (que recebem@Inpute 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 definawidth/heightpara 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
setTimeoutcurto) 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), prefiratransformeopacity.
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 observado | Causa provável | Ação típica |
|---|---|---|
| Rolagem travando em lista | Muitos itens/imagens pesadas | Paginação/infinite scroll, otimizar imagens, trackBy |
| Tela piscando ao atualizar | Re-renderizações | OnPush, evitar recriar objetos, trackBy |
| Toque sem resposta | Thread principal ocupada | Quebrar tarefas, adiar processamento, mostrar loading |
| Carregamento “mudo” | Sem feedback | Skeleton/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).