Noções Básicas de Desempenho
TTI, FPS e orçamentos de memória definidos por nível de aplicativo - o vocabulário que toda equipe React Native precisa antes de ferramentas de profiling, portões de CI ou aprovação de lançamento.
Busque em todas as páginas da documentação
TTI, FPS e orçamentos de memória definidos por nível de aplicativo - o vocabulário que toda equipe React Native precisa antes de ferramentas de profiling, portões de CI ou aprovação de lançamento.
Todos os exemplos abaixo usam APIs e padrões integrados do React Native que funcionam em builds de lançamento do Expo SDK 57. Crie um aplicativo TypeScript padrão para experimentar os trechos de instrumentação.
npx create-expo-app@latest MyPerfApp --template blank-typescript
cd MyPerfApp
npx expo startFerramentas: Estes exemplos visam o Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3.
Time to Interactive (TTI) é o momento em que os usuários podem concluir a ação principal em sua tela principal - não quando a splash screen desaparece, não quando o primeiro pixel é pintado.
// src/perf/tti.ts - contrato definido pela equipe
export type TtiMilestone =
| "app_launch" // início do processo / entrada JS
| "root_layout_mounted"
| "primary_tab_layout"
| "primary_data_ready"
| "primary_interactive";
/**
* Aplicativo de feed de consumidor: inicialização fria → feed inicial rolável + pull-to-refresh funcionando.
* Aplicativo de formulário B2B: inicialização fria → primeiro input focado e teclado pronto.
* Documente SUA aba principal e portão em docs/performance-budgets.md.
*/
export const TTI_CONTRACT = {
start: "app_launch" as const,
end: "primary_interactive" as const,
referenceDevice: "Classe Pixel 6a Android",
budgetMs: 2500,
};Relacionado: Otimização de Tempo de Inicialização - adiando o trabalho que bloqueia o TTI
Instrumente o lançamento no layout raiz e marque como interativo quando a tela principal passar seu portão de dados.
// app/_layout.tsx
import { useEffect, useRef } from "react";
import { Slot } from "expo-router";
const t0 = globalThis.performance?.now?.() ?? Date.now();
export default function RootLayout() {
const reported = useRef(false);
useEffect(() => {
if (reported.current) return;
reported.current = true;
const rootMounted = globalThis.performance?.now?.() ?? Date.now();
console.info("[perf] root_layout_mounted_ms", Math.round(rootMounted - t0));
}, []);
return <Slot />;
}// app/(tabs)/index.tsx - aba principal marca como interativa
import { useEffect, useRef } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
export default function HomeScreen() {
const interactive = useRef(false);
useEffect(() => {
if (interactive.current) return;
interactive.current = true;
// Em produção: analytics.track("tti_primary_interactive", { ms: ... })
console.info("[perf] primary_interactive");
}, []);
return (
<View style={styles.screen}>
<Text style={styles.title}>Home</Text>
<Pressable style={styles.cta}>
<Text style={styles.ctaText}>Ação principal</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, justifyContent: "center" },
title: { fontSize: 24, fontWeight: "700", marginBottom: 16 },
cta: { backgroundColor: "#2563eb", padding: 14, borderRadius: 10, alignItems: "center" },
ctaText: { color: "#fff", fontWeight: "600" },
});useEffect raiz - seja consistente entre as versõesconsole.info é apenas para verificação localReact Native executa dois pipelines de animação/renderização. Confundi-los faz você otimizar a thread errada.
| Métrica | Thread | O que mede |
|---|---|---|
| FPS do JS | JavaScript (Hermes) | Renderização React, lógica de negócios, análise de fetch |
| FPS da UI | Nativo (principal) | Layout, composição, gestos, animações nativas |
// Apenas para desenvolvimento - agite o dispositivo → "Show Perf Monitor" no menu de desenvolvimento
// Observe ambos os contadores enquanto rola uma FlatList pesada
import { FlatList, StyleSheet, Text, View } from "react-native";
const ROWS = Array.from({ length: 500 }, (_, i) => ({ id: String(i), title: `Linha ${i}` }));
export default function FpsDemo() {
return (
<FlatList
data={ROWS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text>{item.title}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: "#e2e8f0" },
});Relacionado: Noções Básicas de Listas e Rolagem - virtualização antes da otimização de FPS
Mire em 60 fps (16.7 ms por quadro). Para auditorias de lançamento em Android de nível médio, ≥ 55 fps sustentados na lista principal é um limite prático.
// scripts/perf/frame-budget.ts - constante de documentação para a equipe
export const FRAME_BUDGET = {
targetFps: 60,
frameBudgetMs: 1000 / 60, // ~16.7
auditFloorFps: 55,
primaryScenarios: [
"home_feed_fast_fling",
"tab_switch_animation",
"modal_open_close",
"pull_to_refresh",
],
} as const;Memória é um teto de sessão, não um instantâneo de inicialização. Meça após o uso realista - 10 minutos navegando em feeds, abrindo configurações, retornando do background.
| Nível do Aplicativo | Exemplo | Teto de RAM inicial (Android de referência) |
|---|---|---|
| Leve | Configurações, formulários simples | ≤ 200 MB |
| Padrão | Feed social, e-commerce | ≤ 350 MB |
| Pesado | Mapas, mídia, pacotes offline | ≤ 500 MB com ADR |
// src/perf/memory-budget.ts
export const MEMORY_BUDGET = {
tier: "standard" as const,
ceilingMb: 350,
soakMinutes: 10,
scenarios: ["scroll_home", "open_profile", "background_2min", "return_and_scroll"],
};Relacionado: Vazamentos de Memória e Rotação de Lista - padrões que estouram os tetos de sessão
Bundles Hermes maiores aumentam o tempo de carregamento de parse/bytecode e a pressão do heap. Trate os MB do bundle principal como parte do TTI, não como uma preocupação separada.
# Exporte o bundle de produção para verificação de tamanho (ajuste o ponto de entrada para seu aplicativo)
npx react-native bundle \
--platform android \
--dev false \
--entry-file node_modules/expo-router/entry.js \
--bundle-output /tmp/index.android.bundle \
--assets-dest /tmp/assets
ls -lh /tmp/index.android.bundleCada toque cruza threads. Conhecer o caminho evita "nós memorizamos tudo, mas ainda está lento".
Toque do usuário
→ Gesto nativo (thread UI)
→ Bridge / evento JSI para thread JS
→ Renderização React + seus manipuladores (Hermes)
→ Confirmação de reconciliação para nativo (Fabric)
→ Layout + pintura (thread UI)
import { Pressable, StyleSheet, Text } from "react-native";
export function SlowButton({ onPress }: { onPress: () => void }) {
return (
<Pressable
style={styles.btn}
onPress={() => {
// Executa na thread JS - bloqueia outro trabalho JS até terminar
let sum = 0;
for (let i = 0; i < 2_000_000; i++) sum += i;
onPress();
}}
>
<Text style={styles.label}>Me toque</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
btn: { padding: 14, backgroundColor: "#2563eb", borderRadius: 8 },
label: { color: "#fff", fontWeight: "600" },
});InteractionManager ou módulos nativosInteractionManager enfileira tarefas até que animações e transições terminem - a primeira alavanca para inicialização sem reescrever recursos.
import { useEffect, useState } from "react";
import { InteractionManager, StyleSheet, Text, View } from "react-native";
export default function HomeWithDeferredInit() {
const [ready, setReady] = useState(false);
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
// Inicialização de analytics, flags de recursos, prefetch não crítico
setReady(true);
});
return () => task.cancel();
}, []);
return (
<View style={styles.screen}>
<Text style={styles.title}>Home</Text>
<Text style={styles.meta}>{ready ? "Telemetria ativa" : "Apenas UI principal"}</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16 },
title: { fontSize: 22, fontWeight: "700" },
meta: { marginTop: 8, color: "#64748b" },
});Antes de ferramentas avançadas, reproduza um script de auditoria fixo em um dispositivo de referência a cada lançamento.
## Auditoria semanal de rolagem (5 minutos)
1. Inicialização fria do aplicativo no Pixel 6a (ou dispositivo de referência da equipe).
2. Abra a aba principal do feed.
3. Role rapidamente por 15 segundos.
4. Registre: FPS da UI min, FPS do JS min, quaisquer linhas em branco.
5. Abra/feche modal no feed - observe picos de re-renderização do pai no DevTools.
6. Compare com o último RC - regressões > 10% acionam investigação.Orçamentos anônimos são ignorados na semana de pico. Copie a tabela inicial para docs/performance-budgets.md e atribua pessoas.
| Métrica | Limite (inicial) | Cargo do Proprietário |
|---|---|---|
| Bundle JS principal (Hermes) | ≤ 4 MB gzipped | Líder da plataforma móvel |
| TTI de inicialização fria (aba principal) | ≤ 2.5 s no Android de referência | Líder da equipe de recursos |
| FPS de rolagem (lista principal) | ≥ 55 fps sustentados | Campeão de desempenho de UI |
| Memória após 10 min de sessão | ≤ 350 MB no Android de referência | Plantão móvel |
// src/perf/budgets.ts - importe constantes em scripts de CI e docs
export const PERF_BUDGETS = {
bundleGzipMb: 4,
ttiMs: 2500,
scrollFpsFloor: 55,
memoryCeilingMb: 350,
} as const;Muitos aplicativos de consumidor visam ≤ 2.5 s de inicialização fria para a aba principal interativa em Android de nível médio. Aplicativos pesados (mapas, mídia offline) precisam de tetos mais altos com justificativa ADR - meça sua linha de base primeiro.
Android de baixo e médio alcance expõe travamentos, armazenamento lento e pressão de memória que iPhones de ponta escondem. Se passar no Android de referência, o iOS geralmente fica bem; o inverso é falso.
Não - o Expo Go envia um shell nativo genérico e um grafo de módulos diferente. Perfis de builds de lançamento (EAS preview ou production) para TTI, tamanho do bundle e memória.
60 fps é o ideal. Para auditorias, 55 fps sustentados na lista principal é um limite prático de lançamento. Breves quedas durante a decodificação de imagem são aceitáveis; FPS do JS sustentado em 0 não é.
Hermes melhora a inicialização via bytecode e reduz o custo de parse em comparação com JSC. Ele não remove o trabalho de renderização do React ou a rotação da lista - você ainda adia JS pesado e virtualiza feeds.
memo aleatório.Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026