Resposta rápida: use JankStats para transformar a sensação de “tela engasgando” em uma métrica objetiva: quantos frames foram perdidos, em qual janela de tempo e sob qual estado da UI. Instale o listener na Window da Activity, associe o estado atual da tela (rota, scroll, filtro, carregamento) e registre agregados em debug, testes ou telemetria. JankStats não substitui o Android Studio Profiler, o StrictMode nem Baseline Profiles e Macrobenchmark. Ele fecha o meio do caminho: medir jank contínuo enquanto o usuário (ou o teste) navega, em vez de depender só de um System Trace pontual.

Quando o feed trava ao carregar a próxima página, o scroll de uma LazyColumn “pula” ou a troca de aba demora um frame a mais do que o esperado, o time costuma discutir opiniões. Alguém diz que é a rede. Outro aponta para recomposição. Outro culpa o celular de teste. Sem uma métrica comum, a discussão se alonga e a correção vira chute.

Jank é o nome curto para frames que não chegaram a tempo. Em 60 Hz, o orçamento aproximado é de ~16,7 ms por frame. Em 90 Hz ou 120 Hz, a margem fica ainda menor. Um atraso pequeno e repetido já é perceptível. O Android expõe APIs de frame timing; o Jetpack JankStats organiza esses sinais em um relatório utilizável por apps Kotlin — inclusive Compose.

Neste guia, você vai configurar JankStats, marcar estado da UI, interpretar relatórios, integrar a métrica a Compose e Macrobenchmark, e montar um fluxo de correção que combina diagnóstico, medição e validação.

O que JankStats mede — e o que não mede

JankStats observa a janela da Activity e classifica frames com base no tempo de produção. Em termos práticos, ele ajuda a responder:

  • esta interação gerou frames atrasados?
  • o problema aparece só no scroll, só no cold start da tela ou o tempo todo?
  • a correção reduziu a taxa de jank no mesmo cenário?

Ele não diz sozinho por que o frame atrasou. Para causa raiz, você ainda precisa de Profiler, System Trace, LeakCanary, StrictMode ou inspeção de código. A divisão útil é:

FerramentaPapel
StrictModeencontra I/O e violações na main thread durante o desenvolvimento
LeakCanaryencontra retenção de memória que pode pressionar o GC
Android Studio Profiler / System Traceexplica onde o tempo foi gasto
Baseline Profiles / Macrobenchmarkmede cenários controlados e startup
JankStatsconta e contextualiza jank enquanto a UI roda

Se você já usa o guia de performance Kotlin, trate JankStats como o instrumento de telemetria de frames perdidos dentro desse mapa.

Dependência e setup mínimo

No Gradle Kotlin DSL do módulo :app:

dependencies {
    implementation("androidx.metrics:metrics-performance:1.0.0")
}

Confirme a versão estável atual na documentação oficial do AndroidX Metrics antes de piná-la em produção. O padrão de integração não muda: criar o JankStats a partir da Window, registrar um listener e manter o objeto vivo enquanto a Activity estiver ativa.

Exemplo enxuto em uma ComponentActivity:

class MainActivity : ComponentActivity() {
    private var jankStats: JankStats? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        jankStats = JankStats.createAndTrack(window) { frameData ->
            if (frameData.isJank) {
                Log.d(
                    "JankStats",
                    "jank=${frameData.isJank} " +
                        "totalNanos=${frameData.frameDurationUiNanos} " +
                        "states=${frameData.states}"
                )
            }
        }

        setContent {
            AppTheme {
                AppNavHost(
                    onRouteChanged = { route ->
                        jankStats?.performanceMetricsStateHolder
                            ?.state
                            ?.putState("route", route)
                    }
                )
            }
        }
    }

    override fun onResume() {
        super.onResume()
        jankStats?.isTrackingEnabled = true
    }

    override fun onPause() {
        jankStats?.isTrackingEnabled = false
        super.onPause()
    }
}

Pontos importantes:

  • habilite o tracking só quando fizer sentido — em geral, onResume/onPause;
  • não logue cada frame em produção — agregue, faça amostragem e envie resumos;
  • associe estado — sem route, scroll, filter ou loading, o relatório vira ruído.

PerformanceMetricsState: o contexto que torna o número útil

Um percentual de jank sem contexto quase não ajuda. “3% de frames ruins” pode ser aceitável no cold start de uma tela pesada e inaceitável no scroll contínuo do feed. O PerformanceMetricsState existe para anexar chaves/valores ao relatório.

Estados úteis em apps brasileiros típicos (fintech, delivery, e-commerce, conteúdo):

  • route — destino do Navigation Compose;
  • list — home, pedidos, busca, carteira;
  • interactionscroll, fling, tab_switch, checkout_step;
  • networkidle, refreshing, paging;
  • device_class — compact/medium/expanded, se layouts adaptativos importam.

Em Compose, você pode atualizar o estado a partir de um efeito:

@Composable
fun TrackScreenJank(
    route: String,
    isRefreshing: Boolean,
    metricsStateHolder: PerformanceMetricsState.Holder
) {
    DisposableEffect(route, isRefreshing) {
        val state = metricsStateHolder.state
        state?.putState("route", route)
        state?.putState(
            "network",
            if (isRefreshing) "refreshing" else "idle"
        )
        onDispose {
            state?.removeState("route")
            state?.removeState("network")
        }
    }
}

Combine isso com o Navigation Compose e com layouts adaptativos quando a mesma rota se comporta diferente em tablet e dobrável.

Como interpretar um relatório de jank

O listener entrega FrameData (e variantes com mais detalhe, conforme a versão). O fluxo mental recomendado:

  1. Confirme que há jank realisJank == true em volume relevante, não um frame isolado.
  2. Olhe o estado — o problema está em route=feed + interaction=scroll?
  3. Reproduza em aparelho representativo — intermediário brasileiro, não só o flagship do time.
  4. Abra o Profiler no mesmo cenário — CPU, memória, System Trace.
  5. Formule uma hipótese estreita — “decode de imagem na main durante fling”, “recomposição por parâmetro instável”, “consulta Room sem dispatcher”.
  6. Corrija e remeça o mesmo estado.

Hipóteses frequentes em Compose:

JankStats + Compose: padrões que funcionam

1. Medir scroll de LazyColumn / LazyGrid

Marque interaction=scroll enquanto LazyListState.isScrollInProgress for verdadeiro. Agregue jank só nessa janela. Assim você não mistura o custo do first composition com o custo do fling.

2. Medir troca de aba e AnimatedContent

Transições são candidatas clássicas a jank. Meça antes e depois de ajustar tween/spring, e não avalie só no emulador com hardware graphics generoso.

3. Medir paging

No Paging 3 com Compose, o pico costuma aparecer quando novos itens chegam e a lista recompõe. Separe estados paging=append e paging=refresh.

4. Não confundir jank com startup

Cold start e first frame são território de Baseline Profiles e Macrobenchmark. JankStats brilha depois que a Activity está viva e o usuário interage.

Integração com Macrobenchmark e CI

Macrobenchmark já mede frame timing em cenários controlados. JankStats complementa de duas formas:

  • no app de produção/debug, para telemetria contínua com amostragem;
  • em testes instrumentados, para assertar que um fluxo crítico não regrediu além de um limite acordado.

Exemplo de política de time:

  • scroll do feed em aparelho de referência: no máximo X% de frames janky em N segundos;
  • troca Home → Detalhe → voltar: orçamento separado;
  • regressão acima do limite bloqueia merge ou abre bug automático.

Use o módulo :benchmark do guia de Baseline Profiles para o cenário controlado e JankStats para o sinal “no app de verdade”. Os dois se reforçam.

O que enviar para observabilidade

Não envie um evento por frame. Prefira agregados por sessão ou por tela:

  • taxa de frames janky;
  • contagem absoluta de jank;
  • p95/p99 de duração de frame, se disponível na sua versão/API;
  • top estados associados (route, interaction);
  • versão do app, modelo de aparelho, taxa de atualização da tela.

No backend ou no provedor de crash/performance, correlacione com Crashlytics e ANR. Jank alto persistente em uma rota costuma preceder reclamações de “app travando”, mesmo sem ANR formal.

Cuidados de privacidade: estado de tela não deve carregar PII. Prefira identificadores de rota (feed, checkout/step2) a títulos com nome de cliente ou query de busca livre.

Fluxo de correção recomendado

  1. Reproduza o engasgo com um roteiro curto (abrir feed → scroll 10 s → abrir detalhe).
  2. Ligue JankStats e confirme jank no estado esperado.
  3. Corra StrictMode em build de debug para descartar disco/rede na main thread.
  4. Capture System Trace / CPU Profiler no mesmo intervalo.
  5. Investigue memória com LeakCanary se o jank cresce ao longo da sessão.
  6. Corrija a causa (dispatcher, estabilidade Compose, tamanho de imagem, paginação, inicialização — veja também App Startup).
  7. Remeça com o mesmo estado e, se possível, com Macrobenchmark.
  8. Só então considere Baseline Profile se o caminho corrigido for crítico no startup/navegação inicial.

Esse ciclo evita a armadilha de “otimizar no escuro” e gera evidência útil em code review.

Erros comuns

  • Ativar JankStats e não anexar estado — você fica com um percentual órfão.
  • Logar tudo em produção — custo de I/O e ruído analítico.
  • Medir só no emulador top de linha — jank real aparece no intermediário.
  • Tratar um frame isolado como regressão — use janelas e taxas.
  • Esperar que JankStats explique alocações ou leaks — use a ferramenta certa para cada pergunta.
  • Otimizar animação sem medir scroll — muitos apps sofrem mais no fling do que no motion de transição.
  • Ignorar taxa de atualização — 120 Hz reduz o orçamento por frame; compare aparelhos equivalentes.

Checklist prático

  • dependência androidx.metrics:metrics-performance no módulo app;
  • JankStats.createAndTrack(window) com listener agregado;
  • tracking ligado em onResume e desligado em onPause;
  • estados route + interaction (e outros relevantes) preenchidos;
  • amostragem definida para builds de release;
  • cenário de scroll crítico coberto por Macrobenchmark ou teste instrumentado;
  • StrictMode ativo em debug;
  • hipótese de causa registrada antes do refactor;
  • comparação antes/depois no mesmo aparelho e build type;
  • sem PII nos estados enviados à telemetria.

Quando JankStats é suficiente — e quando não é

Vale a pena quando o app já tem navegação real, listas, animações ou paging e o time precisa de um sinal contínuo de fluidez. É especialmente útil em produtos com sessão longa (feed, carteira, backlog de pedidos), onde o engasgo aparece depois de alguns minutos.

Não comece por aqui se o app ainda faz I/O na main thread de forma óbvia, se não há testes mínimos ou se o cold start está tão lento que o usuário desiste antes de interagir. Nesse caso, priorize StrictMode, arquitetura de threads, startup e Baseline Profiles.

Perguntas frequentes

JankStats funciona com Jetpack Compose?

Sim. JankStats observa a Window da Activity; Compose renderiza nessa janela. O trabalho extra é anunciar estados (route, scroll, loading) para o relatório fazer sentido.

Qual a diferença entre JankStats e Macrobenchmark?

Macrobenchmark executa cenários controlados e produz métricas reprodutíveis em CI. JankStats pode rodar no app durante uso real ou testes e contextualizar jank com estado da UI. Use os dois.

Preciso de root ou build especial?

Não. É uma biblioteca AndroidX ligada ao app. Ainda assim, interprete resultados com o mesmo cuidado de qualquer métrica de performance: build type, aparelho e taxa de atualização influenciam.

JankStats substitui o GPU Profiler ou Systrace?

Não. Ele quantifica e contextualiza. A explicação fina de threads, binders e pipelines continua no System Trace e no Profiler.

Posso usar isso em app View System (XML)?

Sim. O mecanismo é da Window/Activity. Compose não é pré-requisito; só torna o gerenciamento de estado da UI um pouco mais idiomático via DisposableEffect.

Que taxa de jank é “aceitável”?

Depende do fluxo e do aparelho. Em vez de um número universal, defina orçamentos por cenário crítico e impeça regressões relativas. Um feed comercial costuma ser mais sensível que uma tela de configuração aberta raramente.

Conclusão e próximos passos

JankStats dá ao time Android em Kotlin uma linguagem compartilhada para fluidez: frames perdidos + estado da UI. Com ele, “está engasgando” vira “no scroll do feed, a taxa de jank subiu depois do append do Paging” — e aí o Profiler, o StrictMode e o Compose stability passam a trabalhar sobre uma hipótese clara.

Para fechar o ciclo de performance no app:

  1. leia o Android Studio Profiler com Kotlin;
  2. ative StrictMode nas builds de desenvolvimento;
  3. meça startup e cenários com Baseline Profiles e Macrobenchmark;
  4. investigue retenção com LeakCanary;
  5. estabilize recomposições com o configurador de estabilidade Compose.

Se o objetivo também é mercado de trabalho, jank e performance de UI aparecem em entrevistas Android sênior e em vagas que pedem Compose em produção — vale cruzar com o roadmap Android e as vagas Kotlin.