React Native 性能优化
问题陈述
React Native 性能问题通常源于不必要的重新渲染、未优化的列表和在 JS 线程上进行的昂贵计算。此代码库具有性能关键区域(射击掌握、玩家列表)以及已建立的优化模式。
模式:FlatList 优化
keyExtractor - 稳定的键
// ✅ 正确:稳定的函数引用
const keyExtractor = useCallback((item: Session) => item.id, []);
<FlatList
data={sessions}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
// ❌ 错误:每次渲染都创建新函数
<FlatList
data={sessions}
keyExtractor={(item) => item.id}
renderItem={renderItem}
/>
// ❌ 错误:使用索引(在重新排序/删除时会导致问题)
keyExtractor={(item, index) => `${index}`}
getItemLayout - 固定高度项
const ITEM_HEIGHT = 80;
const SEPARATOR_HEIGHT = 1;
const getItemLayout = useCallback(
(data: Session[] | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: (ITEM_HEIGHT + SEPARATOR_HEIGHT) * index,
index,
}),
[]
);
<FlatList
data={sessions}
getItemLayout={getItemLayout}
// ... 其他属性
/>
为什么重要: 没有 getItemLayout,FlatList 必须测量每个项目,导致滚动时的卡顿。
renderItem - 记忆化
// 提取为命名组件
const SessionItem = memo(function SessionItem({
session,
onPress
}: {
session: Session;
onPress: (id: string) => void;
}) {
return (
<Pressable onPress={() => onPress(session.id)}>
<Text>{session.title}</Text>
</Pressable>
);
});
// 稳定的回调
const handlePress = useCallback((id: string) => {
navigation.push(`/session/${id}`);
}, [navigation]);
// 稳定的 renderItem
const renderItem = useCallback(
({ item }: { item: Session }) => (
<SessionItem session={item} onPress={handlePress} />
),
[handlePress]
);
<FlatList
data={sessions}
renderItem={renderItem}
// ...
/>
其他优化
<FlatList
data={sessions}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
// 性能属性
removeClippedSubviews={true} // 卸载屏幕外项目
maxToRenderPerBatch={10} // 每批渲染的项目数
windowSize={5} // 渲染窗口(屏幕)
initialNumToRender={10} // 初始渲染计数
updateCellsBatchingPeriod={50} // 批量更新延迟(ms)
// 防止额外渲染
extraData={selectedId} // 仅当此变化时重新渲染
/>
模式:FlashList 用于大型列表
何时使用: 1000+ 项目,复杂的项目组件,或 FlatList 仍然卡顿。
import { FlashList } from '@shopify/flash-list';
<FlashList
data={players}
renderItem={renderItem}
estimatedItemSize={80} // 必需 - 估计项目高度
keyExtractor={keyExtractor}
/>
注意: 此代码库当前不使用 FlashList。考虑用于教练玩家列表。
模式:记忆化
useMemo - 昂贵的计算
// ✅ 正确:记忆化昂贵的计算
const sortedAndFilteredItems = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => b.score - a.score)
.slice(0, 100);
}, [items]);
// ❌ 错误:每次渲染都重新计算
const sortedAndFilteredItems = items
.filter(item => item.active)
.sort((a, b) => b.score - a.score);
// ❌ 错误:记忆化简单访问(开销 > 好处)
const userName = useMemo(() => user.name, [user.name]);
何时使用 useMemo:
- 数组转换(filter, sort, map 链)
- 传递给记忆化子组件的对象创建
- O(n) 或更高复杂度的计算
useCallback - 稳定的函数引用
// ✅ 正确:子组件属性的稳定回调
const handlePress = useCallback((id: string) => {
setSelectedId(id);
}, []);
// 传递给记忆化子组件
<MemoizedItem onPress={handlePress} />
// ❌ 错误:useCallback 与不稳定的依赖项
const handlePress = useCallback((id: string) => {
doSomething(unstableObject); // unstableObject 每次渲染都变化
}, [unstableObject]); // 适得其反
何时使用 useCallback:
- 传递给记忆化子组件的回调
- 依赖数组中的回调
- 会导致子组件重新渲染的事件处理程序
模式:React.memo
// 包装接收稳定属性的组件
const PlayerCard = memo(function PlayerCard({
player,
onSelect
}: Props) {
return (
<Pressable onPress={() => onSelect(player.id)}>
<Text>{player.name}</Text>
<Text>{player.rating}</Text>
</Pressable>
);
});
// 复杂属性的自定义比较
const PlayerCard = memo(
function PlayerCard({ player, onSelect }: Props) {
// ...
},
(prevProps, nextProps) => {
// 返回 true 如果属性相等(跳过重新渲染)
return (
prevProps.player.id === nextProps.player.id &&
prevProps.player.rating === nextProps.player.rating
);
}
);
何时使用 React.memo:
- 列表项组件
- 接收稳定原始属性的组件
- 频繁渲染但很少变化的组件
何时不使用:
- 总是接收新属性的组件
- 简单组件(开销 > 好处)
- 根级屏幕
模式:Zustand 选择器优化
问题: 选择整个商店会导致任何状态变化时重新渲染。
// ❌ 错误:在任何商店变化时重新渲染
const store = useAssessmentStore();
// 或
const { userAnswers, isLoading, retakeAreas, ... } = useAssessmentStore();
// ✅ 正确:仅在选定值变化时重新渲染
const userAnswers = useAssessmentStore((s) => s.userAnswers);
const isLoading = useAssessmentStore((s) => s.isLoading);
// ✅ 正确:多个值与浅比较
import { useShallow } from 'zustand/react/shallow';
const { userAnswers, isLoading } = useAssessmentStore(
useShallow((s) => ({
userAnswers: s.userAnswers,
isLoading: s.isLoading
}))
);
另见: rn-zustand-patterns/SKILL.md 了解更多 Zustand 模式。
模式:图像优化
import { Image } from 'expo-image';
// expo-image 提供缓存和性能优化
<Image
source={{ uri: player.avatarUrl }}
style={{ width: 50, height: 50 }}
contentFit="cover"
placeholder={blurhash} // 加载时显示
transition={200} // 淡入持续时间
cachePolicy="memory-disk" // 缓存策略
/>
// 对于列表,添加优先级
<Image
source={{ uri: player.avatarUrl }}
priority={isVisible ? 'high' : 'low'}
/>
模式:避免重新渲染
对象/数组稳定性
// ❌ 错误:每次渲染都创建新对象
<ChildComponent style={{ padding: 10 }} />
<ChildComponent config={{ enabled: true }} />
// ✅ 正确:稳定的引用
const style = useMemo(() => ({ padding: 10 }), []);
const config = useMemo(() => ({ enabled: true }), []);
<ChildComponent style={style} />
<ChildComponent config={config} />
// ✅ 正确:或使用 StyleSheet
const styles = StyleSheet.create({
container: { padding: 10 },
});
<ChildComponent style={styles.container} />
子组件稳定性
// ❌ 错误:内联函数每次渲染都创建新元素
<Parent>
{() => <Child />}
</Parent>
// ✅ 正确:稳定的元素
const child = useMemo(() => <Child />, [deps]);
<Parent>{child}</Parent>
模式:检测重新渲染
React DevTools Profiler
- 打开 React DevTools
- 转到 Profiler 标签
- 点击记录,交互,停止
- 查看 “Flamegraph” 以了解渲染时间
- 查找不必要渲染的组件
why-did-you-render
// 开发中设置
import React from 'react';
if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// 标记特定组件进行跟踪
PlayerCard.whyDidYouRender = true;
控制台日志
// 快速检查重新渲染
function PlayerCard({ player }: Props) {
console.log('PlayerCard render:', player.id);
// ...
}
模式:在主线程之外进行重型计算
问题: JS 线程阻塞导致 UI 卡顿。
// ❌ 错误:阻塞 JS 线程
const result = heavyComputation(data); // 需要 500ms
// ✅ 正确:使用 InteractionManager
import { InteractionManager } from 'react-native';
InteractionManager.runAfterInteractions(() => {
const result = heavyComputation(data);
setResult(result);
});
// ✅ 正确:用于视觉更新的 requestAnimationFrame
requestAnimationFrame(() => {
// 在当前帧后更新
});
性能检查表
在发布列表密集型屏幕之前:
- [ ] FlatList 有
keyExtractor(稳定的回调) - [ ] FlatList 有
getItemLayout(如果是固定高度) - [ ] 列表项使用
React.memo记忆化 - [ ] 传递给项目的回调使用
useCallback - [ ] Zustand 选择器是特定的(不是整个商店)
- [ ] 图像使用
expo-image进行缓存 - [ ] 没有内联对象/函数属性传递给记忆化子组件
- [ ] Profiler 显示没有不必要的重新渲染
常见问题
| 问题 | 解决方案 |
|---|---|
| 列表滚动卡顿 | 添加 getItemLayout,记忆化项目 |
| 组件重新渲染太频繁 | 检查选择器的特定性,记忆化属性 |
| 初始渲染缓慢 | 减少 initialNumToRender,推迟计算 |
| 内存增长 | 检查状态累积,图像缓存 |
| 交互时 UI 冻结 | 将计算移出主线程 |
与其他技能的关系
- rn-zustand-patterns:选择器优化模式
- rn-styling:StyleSheet.create 用于稳定的样式引用