在 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,而不是一次性渲染整个数据集。优化时我一般从几个方面入手:

  1. 控制 Cell 渲染:使用 React.memo,避免无关 Item 重渲染。

  2. 稳定引用renderItemkeyExtractor、callback 使用 useCallback,避免不必要的函数重新创建。

  3. 固定高度优化:如果 Item 高度固定,使用 getItemLayout,减少布局测量,并提升 initialScrollIndex 等场景的定位效率。

  4. 调整虚拟化参数:根据实际场景调整 initialNumToRendermaxToRenderPerBatchupdateCellsBatchingPeriodwindowSize,在内存、首屏速度和快速滚动体验之间做平衡。

  5. 减少 JS Thread 工作:避免在 renderItem 中进行复杂计算。

  6. 优化图片:长列表中的图片加载、解码和缓存往往是性能瓶颈,需要使用合适尺寸的图片并做好缓存。

  7. 减少 data 无意义更新:避免频繁创建新的 data 引用。

  8. 最后通过 Flipper / React DevTools / Android Studio Profiler / Xcode Instruments 等工具在真机上定位到底是 JS、UI 还是图片/内存问题。

一句话记忆:

FlatList 优化
=
虚拟化窗口
+
减少 Cell Render
+
稳定引用
+
getItemLayout
+
Batch 参数调优
+
图片优化
+
减少 JS Thread 工作

这套思路基本覆盖了 React Native 长列表性能优化的核心。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐