Conceptos Básicos de Rendimiento
TTI, FPS y presupuestos de memoria definidos por nivel de aplicación - el vocabulario que todo equipo de React Native necesita antes de las herramientas de perfilado, compuertas de CI o firma de lanzamiento.
Busca en todas las páginas de la documentación
TTI, FPS y presupuestos de memoria definidos por nivel de aplicación - el vocabulario que todo equipo de React Native necesita antes de las herramientas de perfilado, compuertas de CI o firma de lanzamiento.
Todos los ejemplos a continuación utilizan APIs y patrones de React Native integrados que funcionan en compilaciones de lanzamiento de Expo SDK 57. Crea una aplicación TypeScript estándar para probar los fragmentos de instrumentación.
npx create-expo-app@latest MyPerfApp --template blank-typescript
cd MyPerfApp
npx expo startHerramientas: Estos ejemplos están diseñados para Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3.
Tiempo para Interactividad (TTI) es el momento en que los usuarios pueden completar la acción principal en tu pantalla principal - no cuando se oculta el splash, no cuando se pinta el primer píxel.
// src/perf/tti.ts - team-defined contract
export type TtiMilestone =
| "app_launch" // process start / JS entry
| "root_layout_mounted"
| "primary_tab_layout"
| "primary_data_ready"
| "primary_interactive";
/**
* Consumer feed app: cold start → home feed scrollable + pull-to-refresh works.
* B2B form app: cold start → first input focused and keyboard-ready.
* Document YOUR primary tab and gate in docs/performance-budgets.md.
*/
export const TTI_CONTRACT = {
start: "app_launch" as const,
end: "primary_interactive" as const,
referenceDevice: "Pixel 6a class Android",
budgetMs: 2500,
};Relacionado: Optimización del Tiempo de Inicio - diferiendo trabajo que bloquea TTI
Instrumenta el lanzamiento en el diseño raíz y marca interactivo cuando la pantalla principal pasa su compuerta de datos.
// 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 - primary tab marks interactive
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;
// In production: analytics.track("tti_primary_interactive", { ms: ... })
console.info("[perf] primary_interactive");
}, []);
return (
<View style={styles.screen}>
<Text style={styles.title}>Inicio</Text>
<Pressable style={styles.cta}>
<Text style={styles.ctaText}>Acción 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 raíz - sé consistente entre lanzamientosconsole.info es solo para verificación localReact Native ejecuta dos canalizaciones de animación/renderizado. Confundirlas te envía optimizando el hilo incorrecto.
| Métrica | Hilo | Qué mide |
|---|---|---|
| JS FPS | JavaScript (Hermes) | Renderizado de React, lógica empresarial, análisis de captura |
| UI FPS | Nativo (principal) | Diseño, composición, gestos, animaciones nativas |
// Dev only - shake device → "Show Perf Monitor" in dev menu
// Watch both counters while scrolling a heavy FlatList
import { FlatList, StyleSheet, Text, View } from "react-native";
const ROWS = Array.from({ length: 500 }, (_, i) => ({ id: String(i), title: `Row ${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: Conceptos Básicos de Listas y Desplazamiento - virtualización antes de ajuste de FPS
Objetivo 60 fps (16,7 ms por fotograma). Para auditorías de lanzamiento en Android de gama media, ≥ 55 fps sostenido en la lista principal es un piso práctico.
// scripts/perf/frame-budget.ts - documentation constant for the team
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;La memoria es un techo de sesión, no una instantánea de lanzamiento. Mide después del uso realista - 10 minutos navegando feeds, abriendo configuración, volviendo del fondo.
| Nivel de aplicación | Ejemplo | Techo de RAM de inicio (Android de referencia) |
|---|---|---|
| Ligera | Configuración, formularios simples | ≤ 200 MB |
| Estándar | Feed social, comercio electrónico | ≤ 350 MB |
| Pesada | Mapas, medios, paquetes sin conexión | ≤ 500 MB con 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: Fugas de Memoria y Cambio de Lista - patrones que rompen techos de sesión
Los bundles de Hermes más grandes aumentan tiempo de carga de análisis/bytecode y presión de heap. Trata el MB del bundle principal como parte de TTI, no una preocupación separada.
# Export production bundle for size check (adjust entry for your app)
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 tap cruza hilos. Conocer el camino evita "memorizamos todo pero sigue siendo lento."
User tap
→ Native gesture (UI thread)
→ Bridge / JSI event to JS thread
→ React render + your handlers (Hermes)
→ Reconciliation commit to native (Fabric)
→ Layout + paint (UI thread)
import { Pressable, StyleSheet, Text } from "react-native";
export function SlowButton({ onPress }: { onPress: () => void }) {
return (
<Pressable
style={styles.btn}
onPress={() => {
// Runs on JS thread - blocks other JS work until finished
let sum = 0;
for (let i = 0; i < 2_000_000; i++) sum += i;
onPress();
}}
>
<Text style={styles.label}>Tócame</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
btn: { padding: 14, backgroundColor: "#2563eb", borderRadius: 8 },
label: { color: "#fff", fontWeight: "600" },
});InteractionManager o módulos nativosInteractionManager cola tareas hasta que se completen las animaciones y transiciones - la primera palanca para inicio sin reescribir características.
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(() => {
// Analytics init, feature flags, non-critical prefetch
setReady(true);
});
return () => task.cancel();
}, []);
return (
<View style={styles.screen}>
<Text style={styles.title}>Inicio</Text>
<Text style={styles.meta}>{ready ? "Telemetría activa" : "Solo 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 herramientas avanzadas, reproduce un script de auditoría fijo en un dispositivo de referencia cada lanzamiento.
## Weekly scroll audit (5 minutes)
1. Cold start app on Pixel 6a (or team reference device).
2. Open primary feed tab.
3. Fast fling scroll for 15 seconds.
4. Record: UI FPS min, JS FPS min, any blank rows.
5. Open/close modal on feed - note parent re-render spikes in DevTools.
6. Compare to last RC - regressions > 10% trigger investigation.Los presupuestos anónimos se ignoran en la semana de crisis. Copia la tabla de inicio en docs/performance-budgets.md y asigna humanos.
| Métrica | Umbral (inicio) | Rol del propietario |
|---|---|---|
| Bundle JS principal (Hermes) | ≤ 4 MB comprimido | Líder de plataforma móvil |
| TTI de inicio en frío (pestaña principal) | ≤ 2,5 s en Android de referencia | Líder de equipo de características |
| FPS de desplazamiento (lista principal) | ≥ 55 fps sostenido | Campeón de rendimiento de UI |
| Memoria después de sesión de 10 min | ≤ 350 MB en Android de referencia | Móvil en guardia |
// src/perf/budgets.ts - import constants in CI scripts and docs
export const PERF_BUDGETS = {
bundleGzipMb: 4,
ttiMs: 2500,
scrollFpsFloor: 55,
memoryCeilingMb: 350,
} as const;Muchas aplicaciones de consumidor apuntan a ≤ 2,5 s inicio en frío hasta interactivo de pestaña principal en Android de gama media. Las aplicaciones pesadas (mapas, medios sin conexión) necesitan techos más altos con justificación ADR - mide tu línea base primero.
Los Android de gama baja y media exponen jank, almacenamiento lento y presión de memoria que los iPhones insignia ocultan. Si pasa en Android de referencia, iOS generalmente está bien; lo contrario es falso.
No - Expo Go envía un shell nativo genérico y un gráfico de módulos diferente. Perfila compilaciones de lanzamiento (EAS preview o production) para TTI, tamaño de bundle y memoria.
60 fps es el ideal. Para auditorías, 55 fps sostenido en la lista principal es un piso de lanzamiento práctico. Los breves caídas durante decodificación de imagen son aceptables; JS FPS sostenido en 0 no lo es.
Hermes mejora el inicio mediante bytecode y reduce el costo de análisis versus JSC. No elimina el trabajo de renderizado de React ni cambio de lista - aún difiere JS pesado y virtualiza feeds.
memo aleatorio.Versiones de Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 19 jul 2026