React DevTools Profiler
Encontrando re-renderizados evitables en transiciones de navegación - el flujo de trabajo cookbook para apps Expo SDK 57 antes de reescribir pantallas o agregar memo en todas partes.
Busca en todas las páginas de la documentación
Encontrando re-renderizados evitables en transiciones de navegación - el flujo de trabajo cookbook para apps Expo SDK 57 antes de reescribir pantallas o agregar memo en todas partes.
Tarjeta de referencia rápida - lista para copiar y pegar.
Configuración (una vez por sesión)
# Inicia la app con dev client o debug build
npx expo start
# Presiona j en la terminal → abre React Native DevTools en el navegador
# Abre la pestaña Profiler → engranaje → habilita "Record why each component rendered"Script de reproducción - transición de navegación
Patrón de shell de navegación estable
// 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 solo después de que los props del padre sean estables
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" },
});Cuándo usarlo:
React.memo a 40 componentes sin evidencia.Una repro mínima: screenOptions inestables y un nuevo renderItem cada renderizado fuerzan que el feed completo se confirme en cada desenfoque/enfoque de pestaña.
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 el remontaje del shell de pestaña con nuevos object literals - Profiler se iluminará. */
function UnstableShell({ children }: { children: React.ReactNode }) {
const [tab, setTab] = useState<"home" | "search">("home");
// BAD: nuevo objeto cada renderizado - dispara commits del subárbol de navegación
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" },
});Lista de verificación de Profiler para esta pantalla
| Síntoma en la grabación | Causa probable | Solución |
|---|---|---|
Cada PostRow se confirma en el toque de pestaña | El padre Feed se re-renderizó; renderItem inestable o falta memo | useCallback renderItem + memo fila |
UnstableShell confirma todos los hijos | Objeto headerStyle inline | useMemo o StyleSheet |
Las filas se confirman cuando solo opened cambia | Esperado - pasa extraData | Asegúrate de que solo las filas afectadas necesiten trabajo de árbol completo |
| Proveedor en root se confirma en navegación | Valor de contexto recreado | Divide contexto; memoiza objeto value |
// BAD - nuevo valor cada renderizado
<AppContext.Provider value={{ theme, setTheme, cart, setCart }}>
// BETTER - divide lectura/escritura o memoiza slices de valor
const value = useMemo(() => ({ theme, cart }), [theme, cart]);
<AppContext.Provider value={value}>La navegación a menudo lee contexto en barras de pestañas y encabezados. Si el objeto de valor es fresco cada renderizado, cada pantalla bajo el proveedor se confirma.
Los archivos _layout.tsx se re-renderizan cuando cambian los segmentos de ruta. Eleva los proveedores pesados debajo del segmento que cambia, o divide layouts para que los modales de stack no envuelvan todo el árbol de pestaña.
// app/(tabs)/_layout.tsx - mantén aquí los proveedores que no necesitan estado de stack
// app/(tabs)/feed/_layout.tsx - el estado específico de stack permanece en el subárbol de feedCuando una pantalla de stack se abre sobre una pestaña, la pestaña puede mantenerse montada. Si el padre del feed se suscribe a eventos de enfoque de navegación y llama a setState en cada desenfoque, Profiler muestra un pico en todas las filas visibles. Debounce los manejadores de enfoque y evita almacenar objetos de navegación en React state.
| Vista | Úsalo para |
|---|---|
| Flame graph | Encontrar qué padre arrastró a los hijos a un commit |
| Ranked | Tiempo total por componente - comienza aquí para "quién es lento" |
| Timeline | Correlacionando commits con gestos (scroll, cambio de pestaña) |
Categorías de React 19 "Why did this render?":
memo después de que el padre sea estable.renderItem es una nueva referencia de función.Expo SDK 57 se enruta a través de React Native DevTools (presiona j en CLI). Usa la pestaña Profiler incorporada - el flujo de trabajo coincide con React web DevTools para commits y vistas clasificadas.
DevTools se adjunta a compilaciones depurables. Para tiempos similares a release, usa Hermes sampling y Flashlight junto con Profiler - ver Hermes Sampling Profiler y Flashlight Benchmarking.
Solo después de que renderItem, manejadores y contexto del padre sean estables. De lo contrario, Profiler muestra filas comprometiéndose de todas formas - ver List Performance Tuning.
Algo de re-renderizado en enfoque es esperado. Los picos que incluyen el feed completo no lo son - estabiliza screenOptions, tabBarStyle, y proveedores en app/(tabs)/_layout.tsx.
El batching más automático reduce commits duplicados de setState rápido - bien. No arregla props inestables de padres; las reglas de igualdad referencial no cambian.
Sin umbral de ms universal en Profiler - busca commits inesperados (todas las filas en apertura de modal) y outliers clasificados que puedas eliminar. Empareja con medición de FPS en dispositivo para impacto visible por el usuario.
memo, extraData, stable renderItemlazy tabs and mount behaviorStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026