react navite 长列表优化:FlatList / SectionList 原理与优化属性
在 React Native 里做长列表优化,核心可以先记住一句话:
FlatList / SectionList 都不是“把所有数据一次性渲染出来”,而是基于 VirtualizedList 做虚拟化,只渲染当前视口附近的一小部分 Cell。
下面从原理 → 关键属性 → 实战优化 → 常见坑 → 面试回答来梳理。
1. FlatList 的核心原理
FlatList 本质上是:
FlatList
↓
VirtualizedList
↓
ScrollView + Cell 渲染/回收机制
假设有 10,000 条数据:
const data = Array.from({ length: 10000 });
<FlatList
data={data}
renderItem={...}
/>
它不会简单地:
10,000 data
↓
10,000 React Component
↓
10,000 Native View
而是类似:
可视区域
┌─────────────────┐
│ Cell 20 │
│ Cell 21 │
│ Cell 22 │
│ Cell 23 │
│ Cell 24 │
└─────────────────┘
↑ ↑
overscan overscan
只维护:
-
当前可见 Cell
-
可见区域附近的一部分 Cell
-
必要时卸载远离视口的 Cell
因此内存和首次渲染成本不会随着 10000 条数据线性地把所有 View 都创建出来。
2. FlatList 为什么比 ScrollView 更适合长列表?
ScrollView
<ScrollView>
{data.map(item => (
<Item item={item} />
))}
</ScrollView>
本质上:
10000 条数据
↓
10000 个 Item
↓
10000 个 Native View
数据很多时:
-
首屏慢
-
JS 渲染压力大
-
Native View 很多
-
内存高
-
滚动容易掉帧
FlatList
<FlatList
data={data}
renderItem={({ item }) => <Item item={item} />}
/>
采用 Virtualization:
10000 条数据
↓
计算当前可见范围
↓
只创建部分 Cell
↓
滚动
↓
不断创建/卸载/复用附近 Cell
所以:
长列表优先 FlatList,而不是 ScrollView + map。
3. FlatList 的关键优化属性
最重要的是下面几个。
| 属性 | 作用 | 优化重点 |
|---|---|---|
initialNumToRender |
首次渲染多少条 | 控制首屏成本 |
maxToRenderPerBatch |
每批最多渲染多少条 | 控制批量渲染 |
updateCellsBatchingPeriod |
两批渲染之间的间隔 | 平衡 JS / 滚动 |
windowSize |
虚拟化窗口大小 | 控制内存 |
removeClippedSubviews |
移除屏幕外 View | 减少 Native View |
getItemLayout |
提供 Cell 高度/offset | 大幅减少测量成本 |
keyExtractor |
Cell 唯一 key | 避免错误重建 |
extraData |
触发 FlatList 更新 | 控制更新范围 |
initialScrollIndex |
初始滚动位置 | 配合 getItemLayout |
renderItem |
Cell 渲染函数 | 避免重复创建/更新 |
4. initialNumToRender
<FlatList
data={data}
initialNumToRender={10}
/>
表示:
首次渲染多少个 Cell。
例如:
initialNumToRender = 100
页面打开
↓
先创建 100 个 Item
↓
首屏 JS / Native 工作量增加
如果:
initialNumToRender = 5
首屏更轻,但可能出现:
用户快速滑动
↓
需要立即创建新的 Cell
↓
出现白屏 / 空白区域
所以不是越小越好。
通常根据一屏能显示多少条来设置。
例如一屏大约 8 条:
initialNumToRender={10}
是一个比较合理的起点。
5. maxToRenderPerBatch
<FlatList
maxToRenderPerBatch={10}
/>
控制:
每次批量最多渲染多少个 Cell。
例如:
maxToRenderPerBatch = 10
滚动
↓
发现需要新的 Cell
↓
一次最多创建 10 个
太大:
一次渲染很多 Cell
↓
JS Thread 忙
↓
滚动事件得不到及时处理
↓
掉帧
太小:
JS 很轻
但是 Cell 创建速度跟不上滚动
↓
快速滑动可能出现空白
所以本质是:
响应速度 vs 渲染吞吐量
之间的平衡。
6. updateCellsBatchingPeriod
<FlatList
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
/>
表示:
两批 Cell 渲染之间等待多久。
例如:
Batch 1
↓
等待 50ms
↓
Batch 2
↓
等待 50ms
↓
Batch 3
三个参数经常一起调:
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
/>
可以把它理解为:
initialNumToRender
↓
首屏一次加载多少
maxToRenderPerBatch
↓
后续每次加载多少
updateCellsBatchingPeriod
↓
两批之间隔多久
7. windowSize
这是非常重要的虚拟化参数。
<FlatList
windowSize={21}
/>
它控制 VirtualizedList 保留多大的渲染窗口。
可以简单理解为:
上方缓存区域
┌─────────────────┐
│ │
│ Cell │
│ Cell │
├─────────────────┤
│ 可视区域 │
├─────────────────┤
│ Cell │
│ Cell │
└─────────────────┘
下方缓存区域
默认值通常可以理解为以viewport 为单位计算窗口。
例如:
windowSize={21}
意味着维护一个比较大的虚拟化窗口。
windowSize 太大
更多 Cell 保留
↓
滚动更平滑
↓
内存更高
windowSize 太小
Cell 更少
↓
内存更低
↓
快速滚动时可能出现白屏
所以:
windowSize 是内存和滚动体验之间的 trade-off。
8. getItemLayout —— 长列表非常重要
如果你的 Item 高度固定:
Item 1 = 60
Item 2 = 60
Item 3 = 60
...
一定考虑:
const ITEM_HEIGHT = 60;
<FlatList
data={data}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
为什么快?
因为没有 getItemLayout 时,列表需要知道:
第 5000 个 Item 在哪里?
需要依赖前面 Cell 的布局信息。
有了:
offset = ITEM_HEIGHT * index
直接:
5000 × 60 = 300000
就能知道位置。
getItemLayout 的价值
尤其对:
initialScrollIndex={5000}
非常重要。
否则:
跳到第 5000 条
↓
需要大量计算前面的布局
有 getItemLayout:
index = 5000
↓
直接计算 offset
↓
快速定位
9. keyExtractor
推荐:
<FlatList
data={data}
keyExtractor={(item) => item.id}
/>
不要优先使用:
keyExtractor={(item, index) => index.toString()}
尤其是列表会:
insert
delete
sort
filter
的时候。
例如:
A
B
C
D
删除 B:
A
C
D
如果使用 index:
0 → A
1 → B
2 → C
变成:
0 → A
1 → C
2 → D
React 可能把:
原来的 B Component
↓
当成 C 使用
导致不必要的更新甚至状态错乱。
所以最好:
keyExtractor={(item) => item.id}
10. renderItem 是性能核心
错误的写法:
<FlatList
data={data}
renderItem={({ item }) => (
<Item
item={item}
onPress={() => handlePress(item)}
/>
)}
/>
这里每次父组件 render,都可能创建大量新的函数引用。
可以优化成:
const renderItem = useCallback(
({ item }) => (
<Item
item={item}
onPress={handlePress}
/>
),
[handlePress]
);
<FlatList
data={data}
renderItem={renderItem}
/>
同时:
const Item = React.memo(({ item, onPress }) => {
...
});
形成:
Parent render
↓
renderItem 引用稳定
↓
React.memo
↓
没有变化的 Item 不重新 render
11. React.memo 非常重要
例如:
const ListItem = React.memo(({ item }) => {
return (
<View>
<Text>{item.title}</Text>
</View>
);
});
如果:
100 个 Item
父组件因为某个状态更新:
Parent render
↓
FlatList render
没有 React.memo:
Item 1 render
Item 2 render
Item 3 render
...
Item 100 render
有 React.memo:
Parent render
↓
检查 props
↓
只有 props 变化的 Item render
但注意:
<Item
style={{ marginTop: 10 }}
/>
每次 render:
{} !== {}
会产生新的对象引用。
所以更好:
const styles = StyleSheet.create({
item: {
marginTop: 10,
},
});
12. SectionList 的原理
SectionList 可以理解成:
SectionList
↓
VirtualizedSectionList
↓
VirtualizedList
数据结构:
const sections = [
{
title: 'A',
data: ['Apple', 'Amazon'],
},
{
title: 'B',
data: ['Baidu', 'ByteDance'],
},
];
渲染:
<SectionList
sections={sections}
renderItem={({ item }) => <Item item={item} />}
renderSectionHeader={({ section }) => (
<Header title={section.title} />
)}
/>
最终实际上还是:
Header
Item
Item
Header
Item
Item
...
所以:
SectionList 和 FlatList 的底层虚拟化思想基本一致。
区别主要是 SectionList 在 FlatList 的基础上增加了:
-
Section
-
Section Header
-
Section Footer
-
sticky header
-
section 数据管理
13. SectionList 的常见场景
例如通讯录:
A
├─ Alice
├─ Andy
└─ Aaron
B
├─ Bob
└─ Ben
C
├─ Chris
└─ Charlie
适合:
<SectionList
sections={sections}
renderSectionHeader={...}
renderItem={...}
/>
而不是自己:
<ScrollView>
{sections.map(...)}
</ScrollView>
14. removeClippedSubviews
<FlatList
removeClippedSubviews={true}
/>
简单理解:
对屏幕外的 Native View 进行裁剪/移除,以减少 Native 层的负担。
但是这个属性不是:
开启 = 永远更快
某些复杂布局下可能出现:
-
内容消失
-
Android / iOS 行为差异
-
absolute positioning 问题
-
transform / animation 问题
所以应该:
通过真实设备测试,而不是盲目开启。
15. 一个比较完整的优化模板
const ITEM_HEIGHT = 64;
const ListItem = React.memo(({ item, onPress }) => {
return (
<Pressable onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</Pressable>
);
});
function MyList({ data }) {
const handlePress = useCallback((id) => {
console.log(id);
}, []);
const renderItem = useCallback(
({ item }) => (
<ListItem
item={item}
onPress={handlePress}
/>
),
[handlePress]
);
const keyExtractor = useCallback(
(item) => item.id,
[]
);
const getItemLayout = useCallback(
(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
[]
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
initialNumToRender={10}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
windowSize={15}
removeClippedSubviews
/>
);
}
不过这里的参数:
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={15}
不是黄金配置。
应该根据:
Item 复杂度
+
设备性能
+
一屏 Item 数量
+
图片数量
+
快速滑动场景
进行 profiling。
16. 图片往往才是长列表真正的性能杀手
很多时候你会发现:
FlatList 已经优化了
↓
还是卡
问题可能不是 FlatList,而是:
<Item>
<Image source={{ uri }} />
</Item>
每个 Cell 都有:
-
大图
-
网络请求
-
图片 decode
-
resize
-
cache
-
bitmap 内存
例如:
1000 个商品
↓
1000 张 2MB 图片
↓
图片解码 / 内存压力
↓
滚动掉帧
因此长列表优化通常要同时考虑:
FlatList virtualization
+
Item render
+
图片加载
+
JS Thread
+
Native Thread
+
数据更新
17. 不要在 renderItem 里面做重计算
例如:
const renderItem = ({ item }) => {
const result = heavyCalculation(item);
return <Item result={result} />;
};
滚动过程中:
renderItem
↓
heavyCalculation
↓
JS Thread 被占用
↓
掉帧
更好:
数据进入页面
↓
提前计算 / memo
↓
FlatList 只负责展示
例如:
const processedData = useMemo(
() => data.map(processItem),
[data]
);
18. 不要频繁修改 data 引用
例如:
setData([...data]);
即使内容没变:
oldData !== newData
FlatList 可能需要重新处理数据。
更理想的是:
只有真正发生变化时
↓
更新 data
同时配合:
React.memo
useCallback
useMemo
控制更新范围。
19. 一个非常重要的认知:FlatList 不等于 RecyclerView
很多 Android 开发者会把 FlatList 理解成:
RecyclerView
但不要简单认为:
“FlatList 会像 RecyclerView 一样完美复用所有 View。”
React Native 的 VirtualizedList 核心机制更接近:
维护一个有限的 render window
+
Cell virtualization
+
根据滚动位置动态渲染/卸载
不同 RN 版本、Fabric 架构以及平台实现细节也会影响具体行为。
所以面试时最好说:
FlatList 基于 VirtualizedList 做虚拟化,只维护可视区域附近有限数量的 Cell,而不是一次性渲染整个数据集。
比简单说:
“FlatList 是 RecyclerView”
准确得多。
20. 长列表优化可以形成一个排查顺序
如果遇到:
FlatList 1000 条数据滚动卡顿
我一般按这个顺序排查:
① Item 是否复杂?
↓
② Item 是否 React.memo?
↓
③ renderItem 是否稳定?
↓
④ callback / object 是否频繁创建?
↓
⑤ 是否存在大量图片?
↓
⑥ 是否有 JS 重计算?
↓
⑦ data 是否频繁更新?
↓
⑧ 是否可以 getItemLayout?
↓
⑨ 调整 windowSize
↓
⑩ 调整 batch 参数
↓
⑪ 真机 profiling
不要一上来就:
windowSize={5}
或者:
removeClippedSubviews
因为参数优化只能解决一部分问题。
21. 面试版总结
如果面试官问:
“你怎么优化 React Native 的 FlatList?”
可以回答:
FlatList 底层基于 VirtualizedList,通过虚拟化机制只渲染可视区域及附近窗口内的 Cell,而不是一次性渲染整个数据集。优化时我一般从几个方面入手:
控制 Cell 渲染:使用
React.memo,避免无关 Item 重渲染。稳定引用:
renderItem、keyExtractor、callback 使用useCallback,避免不必要的函数重新创建。固定高度优化:如果 Item 高度固定,使用
getItemLayout,减少布局测量,并提升initialScrollIndex等场景的定位效率。调整虚拟化参数:根据实际场景调整
initialNumToRender、maxToRenderPerBatch、updateCellsBatchingPeriod和windowSize,在内存、首屏速度和快速滚动体验之间做平衡。减少 JS Thread 工作:避免在
renderItem中进行复杂计算。优化图片:长列表中的图片加载、解码和缓存往往是性能瓶颈,需要使用合适尺寸的图片并做好缓存。
减少 data 无意义更新:避免频繁创建新的 data 引用。
最后通过 Flipper / React DevTools / Android Studio Profiler / Xcode Instruments 等工具在真机上定位到底是 JS、UI 还是图片/内存问题。
一句话记忆:
FlatList 优化
=
虚拟化窗口
+
减少 Cell Render
+
稳定引用
+
getItemLayout
+
Batch 参数调优
+
图片优化
+
减少 JS Thread 工作
这套思路基本覆盖了 React Native 长列表性能优化的核心。
更多推荐


所有评论(0)