React DevTools Profiler
Encontrando re-renderizações evitáveis em transições de navegação - o fluxo de trabalho do cookbook para aplicativos Expo SDK 57 antes de reescrever telas ou adicionar memo em todos os lugares.
Busque em todas as páginas da documentação
Encontrando re-renderizações evitáveis em transições de navegação - o fluxo de trabalho do cookbook para aplicativos Expo SDK 57 antes de reescrever telas ou adicionar memo em todos os lugares.
Cartão de receita de referência rápida - pronto para copiar e colar.
Configuração (uma vez por sessão)
# Inicie o aplicativo com o cliente de desenvolvimento ou build de depuração
npx expo start
# Pressione j no terminal → abra o React Native DevTools no navegador
# Abra a aba Profiler → engrenagem → habilite "Record why each component rendered"Script de repro - transição de navegação
Padrão de shell de navegação estável
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { useMemo } from "react";
export default function TabLayout() {
const screenOptions = useMemo(
() => ({
headerShown: false,
tabBarActiveTintColor: "#2563eb",
}),
[],
);
return (
<Tabs screenOptions={screenOptions}>
<Tabs.Screen name="index" options={{ title: "Home" }} />
<Tabs.Screen name="search" options={{ title: "Search" }} />
</Tabs>
);
}// src/ui/FeedRow.tsx - memo apenas depois que as props do pai estiverem estáveis
import { memo } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
export type FeedItem = { id: string; title: string };
export const FeedRow = memo(function FeedRow({
item,
onPress,
}: {
item: FeedItem;
onPress: (id: string) => void;
}) {
return (
<Pressable onPress={() => onPress(item.id)} style={styles.row}>
<Text style={styles.title}>{item.title}</Text>
</Pressable>
);
});
const styles = StyleSheet.create({
row: { padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: "#e2e8f0" },
title: { fontSize: 16, fontWeight: "600" },
});Quando usar isso:
React.memo a 40 componentes sem evidências.Um repro mínimo: screenOptions instáveis e um novo renderItem a cada renderização forçam todo o feed a confirmar a cada desfoque/foco da aba.
import { memo, useCallback, useMemo, useState } from "react";
import {
FlatList,
ListRenderItem,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
type Post = { id: string; title: string };
const POSTS: Post[] = Array.from({ length: 120 }, (_, i) => ({
id: String(i),
title: `Post ${i + 1}`,
}));
const PostRow = memo(function PostRow({
post,
onOpen,
}: {
post: Post;
onOpen: (id: string) => void;
}) {
return (
<Pressable onPress={() => onOpen(post.id)} style={styles.row}>
<Text style={styles.title}>{post.title}</Text>
</Pressable>
);
});
function Feed({ posts }: { posts: Post[] }) {
const [opened, setOpened] = useState<string | null>(null);
const onOpen = useCallback((id: string) => setOpened(id), []);
const renderItem: ListRenderItem<Post> = useCallback(
({ item }) => <PostRow post={item} onOpen={onOpen} />,
[onOpen],
);
return (
<View style={styles.panel}>
<Text style={styles.heading}>Feed {opened ? `(opened ${opened})` : ""}</Text>
<FlatList
data={posts}
keyExtractor={(item) => item.id}
renderItem={renderItem}
extraData={opened}
style={styles.list}
/>
</View>
);
}
/** Simula o shell da aba sendo remontado com literais de objeto novos - o Profiler acenderá. */
function UnstableShell({ children }: { children: React.ReactNode }) {
const [tab, setTab] = useState<"home" | "search">("home");
// RUIM: novo objeto a cada renderização - aciona confirmações na subárvore de navegação
const headerStyle = { backgroundColor: tab === "home" ? "#eff6ff" : "#f8fafc" };
return (
<View style={styles.shell}>
<View style={[styles.header, headerStyle]}>
<Pressable onPress={() => setTab("home")}>
<Text>Home</Text>
</Pressable>
<Pressable onPress={() => setTab("search")}>
<Text>Search</Text>
</Pressable>
</View>
{tab === "home" ? children : <Text style={styles.placeholder}>Search placeholder</Text>}
</View>
);
}
export default function ProfilerNavigationDemo() {
const posts = useMemo(() => POSTS, []);
return (
<UnstableShell>
<Feed posts={posts} />
</UnstableShell>
);
}
const styles = StyleSheet.create({
shell: { flex: 1 },
header: { flexDirection: "row", gap: 16, padding: 16 },
panel: { flex: 1 },
heading: { paddingHorizontal: 16, fontWeight: "700" },
list: { flex: 1 },
row: { padding: 16 },
title: { fontSize: 15 },
placeholder: { padding: 16, color: "#64748b" },
});Checklist do Profiler para esta tela
| Sintoma na gravação | Causa provável | Correção |
|---|---|---|
Cada PostRow confirma ao tocar na aba | Pai Feed re-renderizado; renderItem instável ou memo ausente | useCallback renderItem + memo row |
UnstableShell confirma todos os filhos | Objeto headerStyle inline | useMemo ou StyleSheet |
Linhas confirmam quando apenas opened muda | Esperado - passe extraData | Garanta que apenas as linhas afetadas precisem de trabalho completo na árvore |
| Provedor na raiz confirma na navegação | Valor de contexto recriado | Divida o contexto; memoize o objeto value |
// RUIM - novo valor a cada renderização
<AppContext.Provider value={{ theme, setTheme, cart, setCart }}>
// MELHOR - divida leitura/escrita ou memoize fatias de valor
const value = useMemo(() => ({ theme, cart }), [theme, cart]);
<AppContext.Provider value={value}>A navegação frequentemente lê o contexto em barras de abas e cabeçalhos. Se o objeto de valor for novo a cada renderização, cada tela sob o provedor é confirmada.
Arquivos _layout.tsx re-renderizam quando os segmentos de rota mudam. Eleve provedores pesados abaixo do segmento que muda, ou divida layouts para que modais de pilha não envolvam toda a árvore de abas.
// app/(tabs)/_layout.tsx - mantenha provedores que não precisam de estado de pilha aqui
// app/(tabs)/feed/_layout.tsx - estado específico da pilha permanece na subárvore do feedQuando uma tela de pilha abre sobre uma aba, a aba pode permanecer montada. Se o pai do feed se inscrever em eventos de foco de navegação e chamar setState a cada desfoque, o Profiler mostrará um pico em todas as linhas visíveis. Debounce os manipuladores de foco e evite armazenar objetos de navegação no estado do React.
| Visualização | Usar para |
|---|---|
| Flame graph | Encontrar qual pai arrastou filhos para uma confirmação |
| Ranked | Tempo total por componente - comece aqui para "quem está lento" |
| Timeline | Correlacionar confirmações com gestos (rolagem, troca de aba) |
Categorias do React 19 "Why did this render?":
memo após o pai estar estável.renderItem for uma nova referência de função.Expo SDK 57 roteia através do React Native DevTools (pressione j no CLI). Use a aba Profiler incorporada - o fluxo de trabalho corresponde ao React web DevTools para confirmações e visualizações classificadas.
DevTools se conecta a builds depuráveis. Para tempos semelhantes aos de lançamento, use Hermes sampling e Flashlight junto com o Profiler - veja Hermes Sampling Profiler e Flashlight Benchmarking.
Somente depois que renderItem, manipuladores e o contexto pai estiverem estáveis. Caso contrário, o Profiler mostrará linhas confirmando de qualquer maneira - veja List Performance Tuning.
Algumas re-renderizações ao focar são esperadas. Picos que incluem todo o feed não são - estabilize screenOptions, tabBarStyle e provedores em app/(tabs)/_layout.tsx.
Mais batching automático reduz confirmações duplicadas de setState rápidos - bom. Isso não corrige props instáveis de pais; as regras de igualdade referencial não foram alteradas.
Nenhum limite universal de ms no Profiler - procure por confirmações inesperadas (todas as linhas ao abrir um modal) e outliers classificados que você pode eliminar. Emparelhe com medição de FPS no dispositivo para impacto visível ao usuário.
memo, extraData, renderItem estávellazy e comportamento de montagemVersões da Pilha: 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: 16 de jul. de 2026