@[toc]
前言
只要你做过 RN App,基本都遇到过这种画面:
页面上一个长列表,图片多、文字多、而且还伴随一些交互。滑一滑就掉帧,列表像在"喘气",甚至会白屏闪烁一下。
但其实 RN 的列表不是完全做不到丝滑,只是你必须理解它为什么慢、慢在哪里、哪些优化是有效的。
这篇文章就从纯 JS 层、虚拟列表机制、到原生渲染、图片加载,全链路讲透。
长列表为什么会掉帧
要搞清楚如何优化,先说为什么会掉帧。RN 的列表之所以卡,通常来自:
-
JS 线程压力大
- 每一次滚动都会触发 onScroll,JS 线程会堆积大量任务。
- 你的 renderItem 写得复杂,就会出现频繁的组件重渲染。
-
没有正确虚拟化
- FlatList 默认 windowSize = 21(前10 + 当前1 + 后10),屏幕渲染太多不可见 Cell。
- 超出屏幕的 cell 仍然挂在 React tree 中,导致 diff 时间变长。
-
Cell 组件未做 memo
- 每次父组件 state 更新,会导致所有 cell 重新 render。
-
图片加载阻塞
- 未缓存、未用占位图,滚动中反复解码图片,卡顿明显。
-
大数组操作(百万级数据)
- 大数据传给 FlatList 会在 JS 层遍历,导致初始化加载卡、滚动跳动。
下面的章节逐项讲解解决方案。
FlatList 深入解析:keyExtractor、getItemLayout、windowSize
平时我们都会用 FlatList,但真正理解这些参数的并不多。其实只要配对正确,FlatList 的体验能肉眼提升。
keyExtractor:为什么这么重要
React 需要 key 来识别节点,如果 key 不稳定(例如你用了 index),那么:
- 删除 / 插入 item 会导致整批 cell 重渲染
- 图片闪烁、状态丢失、滚动跳动
正确写法:
jsx
const keyExtractor = (item) => item.id.toString();
不要这样写:
jsx
const keyExtractor = (_, index) => index.toString(); // 会导致重渲染和闪烁
getItemLayout:大幅减少测量成本
RN 默认不知道每个 cell 的高度,会边渲染边测量。
你如果能提前告诉它高度(固定高度场景),列表会快非常多。
示例:
jsx
const ITEM_HEIGHT = 80;
const getItemLayout = (_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
});
适用场景:
- 每一行高度相同
- 评论、订单记录、IM 聊天只有小部分样式差异
不适用:
- 复杂瀑布流
- 文本多少不确定(高度变化大)
windowSize:控制屏幕外渲染量
windowSize 默认为 21(前 10、后 10 个屏幕)。
如果你的 cell 较重或图片多,默认 windowSize 会带来巨大开销。
推荐配置:
jsx
windowSize={5} // 前 2 / 后 2 / 当前一屏
在内容复杂的长列表里,这个参数能减压非常明显。
Demo:一个写法合理的 FlatList(支持图片、占位图、memo 化)
下面给出一个可运行的 Demo,它在实际大部分内容型 App 中都能用。
jsx
import React, { memo } from 'react';
import { View, Text, Image, FlatList, StyleSheet } from 'react-native';
const ITEM_HEIGHT = 80;
const Item = memo(({ item }) => {
return (
<View style={styles.item}>
<Image
source={{ uri: item.thumb }}
style={styles.image}
defaultSource={require('./placeholder.png')}
/>
<Text numberOfLines={2} style={styles.text}>{item.title}</Text>
</View>
);
});
export default function App() {
const data = new Array(10000).fill(0).map((_, i) => ({
id: i,
title: `内容标题 ${i}`,
thumb: `https://picsum.photos/id/${i % 100}/200/200`
}));
return (
<FlatList
data={data}
renderItem={({ item }) => <Item item={item} />}
keyExtractor={item => item.id.toString()}
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index
})}
windowSize={5}
initialNumToRender={10}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={30}
/>
);
}
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
height: ITEM_HEIGHT,
padding: 10,
alignItems: 'center',
},
image: {
width: 60,
height: 60,
marginRight: 12,
borderRadius: 6,
backgroundColor: '#eee'
},
text: {
flex: 1,
},
});
这个 Demo 已包含以下优化:
- memo 化的 Item
- getItemLayout
- windowSize 控制渲染数量
- 图片 defaultSource
- initialNumToRender 控制首屏数量
在模拟器中可以看到明显减少掉帧。
什么时候 FlatList 不够?RecyclerListView 是高性能方案
如果你场景像"商城首页---大量图片"、"Feed 流---图文混排"、"百万级数据列表",FlatList 很容易瓶颈。
这时你应该考虑 RecyclerListView。
RecyclerListView 的核心优势
- 强制虚拟化(不让你绕过)
- 数据模型基于原生 Recycler 思路(而不是 React 组件)
- 支持无限列表
- 可以缓存测量结果
- 可以绑定 LayoutProvider(告诉哪些 item 用哪些模板类型)
理论上适用于:
- Feed 流
- 视频 / 图文混排
- 大量 Cell 类型混合
- 需要 sticky headers + 节点复用
RecyclerListView Demo
下面给出一个可运行示例:
jsx
import React from 'react';
import { Dimensions, Text, View, Image } from 'react-native';
import {
RecyclerListView,
DataProvider,
LayoutProvider
} from "recyclerlistview";
const { width } = Dimensions.get("window");
const ITEM_HEIGHT = 80;
export default function App() {
const data = new Array(50000).fill(0).map((_, i) => ({
id: i,
title: `条目 ${i}`,
thumb: `https://picsum.photos/id/${i % 100}/200/200`
}));
const dataProvider = new DataProvider((r1, r2) => r1.id !== r2.id)
.cloneWithRows(data);
const layoutProvider = new LayoutProvider(
() => 0, // 所有 item 用一种布局类型
(type, dim) => {
dim.width = width;
dim.height = ITEM_HEIGHT;
}
);
const rowRenderer = (_, item) => {
return (
<View style={{ flexDirection:'row', height: ITEM_HEIGHT, padding: 10 }}>
<Image
source={{ uri: item.thumb }}
style={{ width: 60, height: 60, marginRight: 10 }}
/>
<Text style={{ flex: 1 }}>{item.title}</Text>
</View>
);
};
return (
<RecyclerListView
layoutProvider={layoutProvider}
dataProvider={dataProvider}
rowRenderer={rowRenderer}
forceNonDeterministicRendering={true}
canChangeSize={true}
/>
);
}
实际跑一下你会发现:
50,000 条数据也能流畅滚动,FlatList 很难做到这一点。
如何避免"无意义的重渲染"
列表掉帧的一半原因来自无效渲染。下面给出实战中最有效的几个技巧:
1. memo + useCallback 必备组合
每个 Cell 都要:
- 用 memo 包裹
- renderItem 用 useCallback 包裹
jsx
const renderItem = useCallback(
({ item }) => <Item item={item} />,
[]
);
2. 避免在 render 内做计算
错误写法:
jsx
renderItem={() => <Item score={heavyCompute()} />}
正确是先 useMemo:
jsx
const computed = useMemo(() => heavyCompute(), [dep]);
3. 不要在列表中 setState
所有和列表无关的逻辑不要放在 FlatList 的 renderItem 里,否则每次滚动都会触发。
4. 图片要有占位图 + 缓存方案
推荐:
- react-native-fast-image
- defaultSource
- local placeholder
图文混排性能优化(重点)
大部分 App 卡顿来自图文混排页面,比如 Feed、商品列表、瀑布流。
几个关键点:
1. 图片要固定尺寸
高度不固定会导致:
- React 多次测量
- 列表跳动
- render 次数增多
固定图片大小能减少测量成本。
2. 文本行数限制
使用 numberOfLines 能大幅减少布局计算。
3. 图片懒加载
使用:
html
defaultSource
fadeDuration
可以减少闪烁。
4. 图片缓存
FastImage 是最常用的方案,不用会明显掉帧。
实战经验:百万级数据的列表
真实项目里我们做过 100 万行的日志浏览页面,解决方案是:
- 首屏加载 100 行
- 分批 append 数据(每次 1000)
- 使用 RecyclerListView
- 所有 cell 使用固定高度
- 文本提前截断
- 禁止滚动回到顶部(否则会触发大量重新计算)
滚动体验非常稳定,只要你控制住 JS 线程的任务量,RN 也能应对百万级数据。
SectionList、FlatList、RecyclerListView:到底应该用哪个?
这是团队争论最多的问题,这里给一套能落地的决策矩阵:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单固定高度列表 | FlatList + getItemLayout | 简单、够用 |
| 图片较多、布局固定 | FlatList + windowSize + memo | 配置好性能不错 |
| 大量复杂图文混排 | RecyclerListView | 原生级滑动体验 |
| 分组列表(Section) | SectionList / FlashList | 分组能力更强 |
| 10w 行以上 | RecyclerListView | RN 官方虚拟化不够强 |
总结
RN 列表的性能优化不是靠"加一个参数"能解决,你需要从:
- 虚拟化(windowSize / getItemLayout)
- 组件结构(memo / useCallback)
- 图片加载(缓存 / 占位图)
- 引擎(RecyclerListView)
四个方面一起进行优化。
只要方法用对,长列表一样能实现接近原生的体验。