引言:为什么列表性能是 React Native 的核心挑战
做移动开发的人都知道,列表渲染大概是最常见、也最容易掉坑的性能场景。社交信息流、电商商品列表、聊天记录、新闻资讯——你能想到的 App 里几乎都离不开列表。而 React Native 在列表性能这块,说实话,一直让开发者又爱又恨。
到了 2026 年,随着 React Native 新架构(Fabric + JSI)成了强制标准,列表组件的生态发生了不小的变化。除了内置的 FlatList,现在还有 Shopify 出品的 FlashList v2(彻底重写的版本)和 LegendApp 推出的 LegendList(纯 JS 方案的新秀)。三者各有各的长处,选哪个、怎么用、怎么调优,是每个 React Native 开发者迟早要面对的问题。
这篇文章会从底层原理讲到实战代码,帮你真正搞清楚这三个列表组件的差异。
FlatList:内置的虚拟化列表
核心工作原理
FlatList 是 React Native 自带的虚拟化列表组件,底层基于 VirtualizedList。核心思路其实挺简单——只渲染屏幕可见区域附近的元素,其余部分用空白占位符代替。用户滚动时,动态创建新进入视口的组件,销毁离开视口的组件。
听起来挺合理的,对吧?
但问题出在「创建」和「销毁」这两个操作上。每当一个新 Item 进入视口,FlatList 都要从头走一遍完整的组件挂载流程——调 render、创建原生视图、计算布局……如果你的列表项稍微复杂一点(有图片、有嵌套组件、带动画),这个过程就会明显变慢。快速滚动的时候,渲染跟不上滚动速度,就会看到空白区域(Blank Area)一闪而过。我相信很多人都被这个问题折腾过。
FlatList 的性能参数调优
好消息是,FlatList 提供了不少参数让你手动调优渲染行为。下面是最关键的几个:
import { FlatList } from 'react-native';
const OptimizedFlatList = () => (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
// 窗口大小:以可视区域高度为单位,上下各渲染多少倍
// 默认 21(上下各 10 倍),降低可省内存,但容易出空白
windowSize={11}
// 每批渲染的 Item 数量
// 越大 → 空白越少,但 JS 线程阻塞越久
maxToRenderPerBatch={5}
// 首次渲染的 Item 数量
initialNumToRender={10}
// 批次渲染间隔(毫秒)
updateCellsBatchingPeriod={50}
// 如果所有 Item 高度固定,提供这个可跳过异步布局计算
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
// 移除可视区域外的原生视图,释放资源
removeClippedSubviews={true}
/>
);
这些参数本质上就是在做一个性能 vs 体验的权衡:
windowSize越大,预渲染越多,空白越少——但内存占用也越高maxToRenderPerBatch越大,填充越快,不过每次渲染阻塞 JS 线程的时间也越长getItemLayout对固定高度列表效果很好,但碰到动态高度就没辙了
FlatList 的固有缺陷
不管你怎么调参数,FlatList 都有几个架构层面绕不过去的问题:
- 没有组件回收机制——Item 滑出视口就销毁,滑回来就重建。大列表里频繁的挂载/卸载操作会严重拖慢 JS 线程
- 空白区域难以根治——快速滚动时渲染跟不上,空白几乎避免不了
- 低端 Android 设备表现很差——iOS 还好,原生视图渲染效率高,问题不太明显;但 Android 上的卡顿感就非常明显了(尤其是那些千元以下的机型)
- 异步布局测量——旧架构中布局计算需要通过异步 Bridge,延迟不可控
所以结论很明确:小型列表(50 条以内)或者对性能不太敏感的场景,FlatList 完全够用。但要是列表有几百上千条数据,或者 Item 比较复杂,FlatList 就开始力不从心了。
FlashList v2:新架构下的性能王者
从 v1 到 v2 的蜕变
FlashList 是 Shopify 团队开发的高性能列表组件。v1 版本就已经凭借视图回收机制做到了比 FlatList 快 5-10 倍。而 2025 年发布的 v2 是一次彻底的重写,专门为新架构打造,带来了三个很重要的变化:
- 不再需要尺寸估算——v1 要你提供
estimatedItemSize,估算不准就会导致布局抖动;v2 利用新架构的同步布局测量能力,自动精确计算尺寸 - 纯 JS 实现——v2 彻底移除了原生依赖,iOS、Android、Web 上行为完全一致
- 内置瀑布流——不再需要单独的
MasonryFlashList,一个masonryprop 就搞定
视图回收:FlashList 的核心秘诀
FlatList 和 FlashList 最根本的区别在于怎么处理离开视口的组件。FlatList 的做法是销毁再重建——Item 滚出去就卸载,滚回来就挂载一个全新的实例。
FlashList 则走了视图回收(View Recycling)这条路。它维护一个组件池,Item 滑出屏幕时组件不销毁,而是放回池里。新 Item 要渲染时,直接从池里取一个组件,更新它的 props 就行——比从零创建一个新组件快太多了。
打个比方吧:FlatList 像是每次需要碗就现烧一个,用完就砸碎;FlashList 像是有一柜子碗,用完洗干净放回去,下次直接拿。(我觉得这个比喻还挺形象的。)
FlashList v2 实战示例
基础用法
import { FlashList } from "@shopify/flash-list";
import { View, Text, Image, StyleSheet } from "react-native";
import { useCallback } from "react";
interface FeedItem {
id: string;
title: string;
description: string;
imageUrl: string;
author: string;
}
export default function SocialFeed({ data }: { data: FeedItem[] }) {
// v2 中 memoize renderItem 非常重要!
const renderItem = useCallback(({ item }: { item: FeedItem }) => (
<View style={styles.card}>
<Image source={{ uri: item.imageUrl }} style={styles.image} />
<View style={styles.content}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description} numberOfLines={2}>
{item.description}
</Text>
<Text style={styles.author}>{item.author}</Text>
</View>
</View>
), []);
const keyExtractor = useCallback((item: FeedItem) => item.id, []);
return (
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
);
}
const styles = StyleSheet.create({
card: { backgroundColor: "#fff", borderRadius: 12, marginBottom: 12, overflow: "hidden" },
image: { width: "100%", height: 200 },
content: { padding: 16 },
title: { fontSize: 18, fontWeight: "bold", marginBottom: 4 },
description: { fontSize: 14, color: "#666", marginBottom: 8 },
author: { fontSize: 12, color: "#999" },
});
看到没?API 跟 FlatList 几乎一模一样。但底层的视图回收机制让性能直接提升了一个量级。在 Shopify 的实际测试中,JS 线程 CPU 使用率从超过 90% 降到了不到 10%。这个数字确实很夸张,不过人家有生产环境的数据支撑。
瀑布流布局(Masonry)
v2 最让人兴奋的新功能之一就是内置瀑布流。v1 的时候你得导入单独的 MasonryFlashList,v2 只需要加一个 prop:
import { FlashList } from "@shopify/flash-list";
export default function PinterestGrid({ data }) {
return (
<FlashList
data={data}
numColumns={2}
masonry // 就这么简单!
renderItem={({ item }) => (
<View style={{ margin: 4, borderRadius: 8, overflow: "hidden" }}>
<Image
source={{ uri: item.imageUrl }}
style={{ width: "100%", height: item.height }}
/>
<Text style={{ padding: 8 }}>{item.title}</Text>
</View>
)}
keyExtractor={(item) => item.id}
/>
);
}
要是某些 Item 需要跨列显示(比如精选内容),可以通过 overrideItemLayout 设置 span:
<FlashList
data={data}
numColumns={3}
masonry
overrideItemLayout={(layout, item) => {
if (item.type === "featured") {
layout.span = 2; // 精选项占两列
}
}}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
维持滚动位置(Chat 场景)
FlashList v2 默认启用了 maintainVisibleContentPosition,列表顶部插入新内容时滚动位置不会跳来跳去。这对聊天界面和实时数据流来说太重要了。
从 v1 迁移到 v2 的注意事项
如果你的项目正在用 FlashList v1,迁移到 v2 时有几个关键点要注意:
- 移除
estimatedItemSize——v2 不需要也不会读取尺寸估算了 - 替换
MasonryFlashList——改成FlashList加masonryprop - 确保 props 已 memoize——v2 不再像 v1 那样帮你自动做选择性更新,
renderItem必须用useCallback包起来 - 提供
keyExtractor——v2 强烈建议用有效的keyExtractor,不然向上滚动可能出现布局闪烁 - 移除废弃的 props——
onBlankArea、disableHorizontalListHeightMeasurement、disableAutoLayout在 v2 中都不支持了 - 前提条件——项目必须跑在 React Native 新架构上(0.76+),v2 不支持旧架构
LegendList:纯 JS 新秀的独特价值
为什么还需要另一个列表组件?
你可能会想:有了 FlashList v2 就够了吧?其实不然。LegendList 由 LegendApp 团队开发,它填补了一个很实际的需求:一个同时支持新旧架构、100% 纯 JS、专门为复杂动态场景设计的列表组件。
不像 FlashList v2 强制要求新架构,LegendList 在旧架构项目中也能正常跑。对于还在迁移过程中的团队来说,这一点真的很关键。
核心特性
双向无限滚动
LegendList 最亮眼的特性是原生支持双向无限滚动,而且没有闪烁、没有位置跳动。做过聊天应用的人应该都懂这有多难——用户往上滑加载历史消息时,当前阅读位置必须纹丝不动。
import { LegendList } from "@legendapp/list";
export default function ChatScreen({ messages, onLoadMore }) {
return (
<LegendList
data={messages}
renderItem={({ item }) => <ChatBubble message={item} />}
keyExtractor={(item) => item.id}
// 内容对齐到底部——专为聊天场景设计
alignItemsAtEnd
// 维持可见内容位置,防止加载历史消息时跳动
maintainVisibleContentPosition
// 启用视图回收以提升性能
recycleItems={true}
// 到达顶部时加载更多历史消息
onStartReached={onLoadMore}
// 预渲染缓冲区(像素)
drawDistance={300}
/>
);
}
重点看一下 alignItemsAtEnd 这个 prop——它让列表内容从底部对齐,消息少的时候自动在顶部补上空白。也就是说你不需要用 inverted 来反转列表了,直接避开了反转列表在动画和手势交互方面那些让人头疼的 Bug。如果你做过聊天 UI,应该知道 inverted 有多少坑。
可选的视图回收
FlashList 默认就开启视图回收,而 LegendList 把选择权交给了你:
recycleItems={false}(默认)——每次创建新组件,更安全但性能差一些。适合 Item 内部有复杂 state 的场景recycleItems={true}——回收组件实例,性能更好。不过 Item 如果有本地 state 的话,可能会碰到状态被错误复用的问题
// 如果 Item 有本地状态,不开回收更安全
<LegendList
data={data}
renderItem={({ item }) => <StatefulCard item={item} />}
recycleItems={false} // 默认值
/>
// 如果 Item 是纯展示型,开启回收提升性能
<LegendList
data={data}
renderItem={({ item }) => <SimpleCard item={item} />}
recycleItems={true}
/>
调试友好的 API
LegendList 的内部滚动状态 API 提供了丰富的调试数据,包括一个 positionAtIndex 函数,可以拿到任意索引的精确滚动位置。配合 useRef 来用:
import { LegendList, LegendListRef } from "@legendapp/list";
import { useRef } from "react";
function MyList() {
const listRef = useRef<LegendListRef>(null);
const scrollToMessage = (index: number) => {
listRef.current?.scrollToIndex({ index, animated: true });
};
return (
<LegendList
ref={listRef}
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
);
}
三者对比:到底该选哪个
性能对比概览
| 维度 | FlatList | FlashList v2 | LegendList |
|---|---|---|---|
| 渲染策略 | 虚拟化(创建/销毁) | 视图回收(默认开启) | 可选回收 / 虚拟化 |
| 相对 FlatList 性能 | 基准 | 快 5-10 倍 | 快 2-5 倍 |
| 是否需要原生依赖 | 内置 | 否(v2 纯 JS) | 否(纯 JS) |
| 新架构要求 | 无 | 必须 | 新旧都支持 |
| 需要尺寸估算 | getItemLayout 可选 | 不需要 | estimatedItemSize 可选 |
| 瀑布流支持 | 无 | 原生 masonry prop | 无 |
| 双向无限滚动 | 有限支持 | 支持 | 专门优化 |
| 聊天列表底部对齐 | 需 inverted hack | maintainVisibleContentPosition | alignItemsAtEnd 原生支持 |
| Web 支持 | 有限 | 良好 | 良好 |
| 社区生态 | 最成熟 | Shopify 维护,月下载 200 万+ | 较新,快速成长中 |
场景化选型建议
什么时候选 FlatList:
- 列表数据量小(50 条以内),Item 也不复杂
- 项目还没升级到新架构,短期内也没有升级计划
- 想要最少的依赖——FlatList 是内置的,零额外安装
- 简单的设置页面、选项列表等对性能要求不高的场景
什么时候选 FlashList v2:
- 项目已经在新架构上跑了
- 社交信息流、电商商品列表、新闻资讯流这类标准大列表场景
- 需要瀑布流/网格布局(类似小红书、Pinterest 那种)
- 对极致滚动性能有硬性要求,特别是要兼顾低端 Android 设备
- 希望 API 和 FlatList 尽量兼容,迁移成本低
什么时候选 LegendList:
- 聊天应用——双向无限滚动、底部对齐、不用 inverted,这三个组合在一起真的很香
- 项目还在旧架构但想要比 FlatList 更好的列表性能
- 列表 Item 有复杂本地状态,需要精确控制是否回收
- 实时数据频繁更新的场景(直播列表、股票行情之类的)
- 追求零原生依赖,想让集成和调试都更简单
通用列表性能优化最佳实践
不管你最终选了哪个列表组件,下面这些优化技巧都是通用的。
1. 永远 memoize renderItem
这条是最基本的,也是最容易被忽略的。内联箭头函数会在父组件每次重渲染时被重新创建,结果就是列表里所有 Item 全部跟着重渲染一遍:
// ❌ 错误:每次渲染都重新创建函数
<FlashList
data={data}
renderItem={({ item }) => <Card item={item} />}
/>
// ✅ 正确:用 useCallback 缓存
const renderItem = useCallback(
({ item }) => <Card item={item} />,
[]
);
<FlashList data={data} renderItem={renderItem} />
2. 列表项组件尽量简洁
列表的性能瓶颈往往不在列表组件自身,而在你写的列表项组件。几个实用的优化方向:
- 减少嵌套——每多一层
View就多一次布局计算 - 列表中用缩略图——别在列表里加载原图,进详情页再说
- 别在
renderItem里做重计算——移到数据层或者用useMemo - 用
React.memo包裹列表项(没用 React Compiler 的话)
// ✅ 推荐:简洁的列表项 + React.memo
const FeedCard = React.memo(({ item }: { item: FeedItem }) => (
<View style={styles.card}>
<Image
source={{ uri: item.thumbnailUrl }} // 用缩略图!
style={styles.thumbnail}
/>
<Text style={styles.title}>{item.title}</Text>
</View>
));
// 2026 年用了 React Compiler 的话,可以不手动 memo
// 编译器会自动搞定组件 memoization
3. 善用 getItemType 区分类型
如果列表里有多种类型的 Item(广告卡片、普通卡片、分隔线什么的),一定要告诉列表组件它们的类型。这样回收池就能按类型分组,不会把广告卡片回收后拿去渲染普通内容:
<FlashList
data={feedData}
renderItem={({ item }) => {
switch (item.type) {
case "ad": return <AdCard item={item} />;
case "post": return <PostCard item={item} />;
case "story": return <StoryCard item={item} />;
}
}}
getItemType={(item) => item.type} // 告诉 FlashList 有多种类型
keyExtractor={(item) => item.id}
/>
4. 别在 Item 里用 key prop
这个坑说真的很多人都不知道。如果你在列表项组件内部加了 key prop,React 会强制把组件销毁后重建,视图回收机制就完全失效了。FlashList 和 LegendList 的性能优势白白浪费:
// ❌ 严禁:在 Item 内部用 key
const renderItem = ({ item }) => (
<View key={item.id}> {/* 这会破坏回收! */}
<Text>{item.title}</Text>
</View>
);
// ✅ 正确:key 交给列表组件通过 keyExtractor 管理
const renderItem = ({ item }) => (
<View>
<Text>{item.title}</Text>
</View>
);
5. 图片优化是重中之重
图片加载基本上是列表卡顿的头号元凶。建议这么做:
- 用
expo-image或react-native-fast-image代替默认Image——缓存策略和加载性能都好很多 - 渐进式加载——先显示模糊占位图(BlurHash),图片加载完再淡入
- 控制请求尺寸——让后端返回跟显示尺寸匹配的图片,别动不动就拉原图
新架构如何从根本上改变列表性能
React Native 新架构(Fabric + JSI + TurboModules)对列表性能的提升是质变级别的。理解这个背景对选型思路很有帮助。
同步布局测量
旧架构里,测量一个 View 的尺寸得通过异步 Bridge——发出请求、等结果返回,中间的延迟不可预测。新架构通过 JSI 做到了同步布局测量:组件渲染后在 useLayoutEffect 里就能立刻拿到精确尺寸。
这也是 FlashList v2 能够甩掉尺寸估算的根本原因——它在 useLayoutEffect 里测量实际尺寸,计算精确位置,赶在浏览器绘制之前完成修正。用户看到的始终是布局正确的列表。
JSI 消除序列化开销
旧架构中 JS 线程和 UI 线程通信要做 JSON 序列化/反序列化,对于频繁滚动的列表来说,这是一笔持续不断的性能税。新架构的 JSI 让 JS 直接持有 C++ 对象引用,调方法不用序列化——用之前流行的比喻来说,就是从「写信」变成了「打电话」。
React Compiler 的助力
2026 年做列表优化还多了一个新武器:React Compiler。它在编译阶段就自动分析组件的数据流,注入最优的 memoization 逻辑。实际意义是:
- 不用再手动给每个列表项组件包
React.memo了 useMemo和useCallback也不是必须手写的了——编译器会代劳- 但理解原理依然重要——编译器能优化常见模式,碰到复杂场景还是得手动调
实战案例:从 FlatList 迁移到 FlashList v2
说了这么多理论,来看一个完整的迁移案例。假设你有个电商商品列表,目前用 FlatList,滚动卡顿比较明显:
迁移前(FlatList)
import { FlatList, View, Text, Image, StyleSheet } from "react-native";
export default function ProductList({ products }) {
return (
<FlatList
data={products}
numColumns={2}
keyExtractor={(item) => item.id}
getItemLayout={(data, index) => ({
length: 280,
offset: 280 * Math.floor(index / 2),
index,
})}
windowSize={11}
maxToRenderPerBatch={6}
removeClippedSubviews={true}
renderItem={({ item }) => (
<View style={styles.productCard}>
<Image source={{ uri: item.image }} style={styles.productImage} />
<Text style={styles.productName}>{item.name}</Text>
<Text style={styles.productPrice}>¥{item.price}</Text>
</View>
)}
/>
);
}
迁移后(FlashList v2)
import { FlashList } from "@shopify/flash-list";
import { View, Text, Image, StyleSheet } from "react-native";
import { useCallback } from "react";
export default function ProductList({ products }) {
const renderItem = useCallback(({ item }) => (
<View style={styles.productCard}>
<Image source={{ uri: item.image }} style={styles.productImage} />
<Text style={styles.productName}>{item.name}</Text>
<Text style={styles.productPrice}>¥{item.price}</Text>
</View>
), []);
const keyExtractor = useCallback((item) => item.id, []);
return (
<FlashList
data={products}
numColumns={2}
renderItem={renderItem}
keyExtractor={keyExtractor}
// 不再需要 getItemLayout、windowSize、maxToRenderPerBatch!
// FlashList v2 自动处理这些
/>
);
}
迁移步骤总结:
- 安装——
npm install @shopify/flash-list@^2.0.0 - 换导入——从
react-native的FlatList改成@shopify/flash-list的FlashList - 删掉手动优化参数——
getItemLayout、windowSize、maxToRenderPerBatch、removeClippedSubviews统统不要了 - memoize 回调——
renderItem和keyExtractor用useCallback包一下 - 实机测试——找台低端 Android 设备滚一滚,感受一下性能差异
整个过程通常几分钟就能搞定,效果却很明显。
常见问题
FlashList v2 能在旧架构项目中使用吗?
不行。FlashList v2 是专门给新架构做的,必须 React Native 0.76+ 且启用了新架构。如果你还在旧架构上,有两条路:继续用 FlashList v1.x,或者试试同时支持新旧架构的 LegendList。
FlatList 还值得用吗?要不要全换成 FlashList?
小型简单列表的话,FlatList 完全没问题。二三十条数据、Item 也不复杂,FlatList 性能足够,没必要多引入一个依赖。但数据量大或者 Item 复杂的场景,还是建议迁移到 FlashList v2 或 LegendList。
LegendList 和 FlashList v2 哪个性能更好?
单论原始滚动性能,FlashList v2 通常更优——默认视图回收加上 Shopify 大规模生产验证,底子很扎实。但 LegendList 在动态高度和双向滚动方面有独到的优势,空白闪烁也相对更少。老实说,对大多数项目而言,两者的性能差距不会是决定性因素,更重要的是看功能需求匹配度。
用了 React Compiler 还需要手动 memoize 列表项吗?
如果项目启用了 React Compiler(React 19+),编译器会在编译时自动注入 memoization,大多数情况下不用再手动写 React.memo、useMemo、useCallback。不过搞清楚原理还是有必要的——编译器处理的是常见模式,复杂场景(ESLint 规则会提醒你的)还是要自己来。
从 FlatList 迁移到 FlashList v2 改动大吗?
不大。FlashList 的 API 和 FlatList 高度兼容,基本就三步:换导入、用 useCallback 包 renderItem、删掉那些不再需要的手动优化参数。多数情况下几分钟就搞定了。