React Native 性能优化列表
jsx // ❌ 不良实践 {data.map(item => ( ))}
`ScrollView` 会一次性渲染所有子元素,即使它们不在屏幕可见区域内。当列表有几百条数据时,内存和 CPU 会瞬间飙升。
**`FlatList`** 是 React Native 提供的专门为长列表设计的组件。它只渲染当前可见区域的少量元素(懒加载),并且支持大量性能优化属性。
```jsx
// ✅ 推荐做法
<FlatList
data={data}
renderItem={({ item }) => <MyComponent item={item} />}
keyExtractor={item => item.id}
/>
3. 掌握 FlatList 的关键性能属性
FlatList 提供了多个直接影响性能的配置项,很多开发者从未调整过,这恰恰是优化的金矿。
| 属性 | 作用 | 推荐值或策略 |
|---|---|---|
windowSize |
渲染窗口的尺寸,以当前屏幕高度为单位,默认是 21(10 屏之上,10 屏之下) | 大幅缩小到 5~10,减少离屏渲染量 |
maxToRenderPerBatch |
每次批量渲染的最大元素数 | 从默认 10 减小到 5,避免一次处理过多组件 |
initialNumToRender |
初始渲染的元素数量 | 刚好铺满一屏即可,例如 6~8 |
removeClippedSubviews |
是否移除屏幕外子组件的原生视图(仅 Android 有效,iOS 也已支持) | 开启 true,可以显著释放原生内存 |
getItemLayout |
如果所有列表项高度固定,预先提供高度信息,避免动态测量 | 固定高度时必须提供,性能提升巨大 |
keyExtractor |
用于生成唯一 key,帮助列表确定元素变化 | 必须提供稳定且唯一的 key,严禁用 index |
3.1 windowSize 调优
windowSize 决定了 FlatList 在屏幕外渲染多少屏的内容。默认为 21,意味着它会渲染当前屏幕上方 10 屏和下方 10 屏的内容,这太激进了。将其设为 5(即上下各保留 5 屏)通常足够流畅。
<FlatList
windowSize={5}
// ...
/>
3.2 getItemLayout 消除测量开销
如果你的列表项高度是固定的(例如每条高度 100),一定要提供 getItemLayout。这样 FlatList 无需渲染每一个项再去测量高度,跳过布局计算,滚动时直接计算出偏移量。
const ITEM_HEIGHT = 100;
<FlatList
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
// ...
/>
如果列表项高度不固定,可以使用 onScrollToIndexFailed 等处理异常,或者采用 FlashList 这类支持动态高度的库。
3.3 removeClippedSubviews
该属性开启后,超出渲染窗口的组件会从原生视图树中移除(相当于在原生端做了 removeFromSuperview),可以明显减少内存占用和原生绘制压力。
<FlatList
removeClippedSubviews={true}
// ...
/>
4. 优化列表项组件:杜绝无意义渲染
即使 FlatList 的属性设置得当,如果列表项组件本身频繁重渲染,性能也会大打折扣。每次 renderItem 被调用时,都会产生新的 JSX,可能触发整个子组件的重新渲染。
4.1 使用 React.memo 包裹 Item 组件
将列表项提取为独立组件,并使用 React.memo 进行浅比较,防止 props 未变化时的渲染。
const ListItem = React.memo(({ item, onPress }) => {
return (
<TouchableOpacity onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</TouchableOpacity>
);
});
4.2 避免在 renderItem 中创建内联函数或对象
在 renderItem 里直接写箭头函数或字面量,每次渲染都会创建新的引用,导致 React.memo 失效。
// ❌ 差劲写法
<FlatList
renderItem={({ item }) => (
<ListItem
item={item}
onPress={() => handlePress(item.id)} // 每次都是新函数
style={{ margin: 10 }} // 每次都是新对象
/>
)}
/>
// ✅ 优化方案:使用 useCallback 和外部常量
const onPressItem = useCallback((id) => {
// 处理点击
}, []);
// 将静态样式提到组件外部
const itemStyle = { margin: 10 };
<FlatList
renderItem={({ item }) => (
<ListItem
item={item}
onPress={onPressItem}
style={itemStyle}
/>
)}
/>
对于包含多个回调的场景,可以使用 useCallback 并依赖 item id,或者用 dispatch 模式,确保传入的回调引用稳定。
4.3 确保 keyExtractor 的唯一和稳定
很多教程用 index 作为 key,这在列表数据被重新排序、过滤或增删时会导致组件状态错乱和性能问题。务必为每一条数据提供唯一的 ID。
keyExtractor={item => item.id.toString()}
5. 图片和媒体加载优化
列表中的图片往往是性能的最大瓶颈:解码图片、加载网络资源都会消耗大量内存和 CPU。滚动时如果大量图片同时加载,必然导致卡顿。
5.1 使用快速图片库
React Native 内置的 Image 组件在加载网络图片和缓存方面表现一般。推荐使用 react-native-fast-image,它利用原生端强大的缓存机制,加载速度更快,并可设置优先级。
import FastImage from 'react-native-fast-image';
<FastImage
style={{ width: 80, height: 80 }}
source={{ uri: item.avatar, priority: FastImage.priority.normal }}
resizeMode={FastImage.resizeMode.cover}
/>
5.2 懒加载与占位图
只在列表项进入可见区域时才加载图片。FlatList 提供了 onViewableItemsChanged 回调,可以结合状态控制图片的加载,但更简单的是使用 react-native-lazy-load 之类的库,或利用 FastImage 的 loading 属性结合骨架屏。
至少要为每张图设置固定尺寸和占位背景,避免加载完成后引起的布局跳动。
<FastImage
style={{ width: 80, height: 80, backgroundColor: '#E1E9EE' }}
source={{ uri: item.avatar }}
/>
5.3 滚动停止后再加载图片
当用户快速滑动列表时,可以暂停图片的加载,等列表停止滚动后再开始。这能显著提升滚动体验。
import { InteractionManager } from 'react-native';
const [shouldLoadImage, setShouldLoadImage] = useState(false);
// 在 onMomentumScrollEnd 或使用 onScroll 节流
onMomentumScrollEnd={() => {
InteractionManager.runAfterInteractions(() => {
setShouldLoadImage(true);
});
}}
6. 分页加载与滚动事件节流
大数据集不可能一次性全部加载,使用分页是必然选择。结合 onEndReached 和 onEndReachedThreshold 实现无限滚动。
<FlatList
data={data}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
// 注意:onEndReached 可能被多次触发,需要加锁
/>
优化点:
- 在触发
loadMore时立刻设置标志位,避免重复请求。 - 滚动事件的回调(若需要)必须节流,比如每 16ms 处理一次,避免高频触发状态更新。
import { throttle } from 'lodash';
const handleScroll = throttle((event) => {
// 处理滚动位置
}, 16);
7. 进阶:使用高性能列表库
如果 FlatList 优化到极致仍不能满足需求(例如秒杀场景、聊天消息列表、动态高度),可以迁移到更专业的列表库。
7.1 FlashList(推荐)
由 Shopify 开发,基于回收机制,比 FlatList 快 5~10 倍。它重用已卸载的列表项组件,极大减少 JS 线程负载,且支持动态高度、粘性头部等。
npm install @shopify/flash-list
使用上与 FlatList 类似,只需额外提供 estimatedItemSize(估算高度)。
import { FlashList } from "@shopify/flash-list";
<FlashList
data={data}
renderItem={({ item }) => <ListItem item={item} />}
estimatedItemSize={200}
/>