@[toc]
很多 RN 项目一遇到列表卡顿,第一反应几乎都是一句话:
FlatList 不行,RN 天生慢。
但如果你真的把性能工具打开,把 JS / UI / Native 三条线程的调用链一层层扒开,会发现一个很扎心的事实:
FlatList 只是最后背锅的那一环,真正拖慢你列表的,往往是你自己写的状态和渲染路径。
这篇文章我们不讨论「FlatList 有什么参数可以调」,而是反过来,从一次最普通的交互开始,把 RN 列表性能问题的真实触发链路完整走一遍。
一、先统一认知:FlatList 到底干了什么?
在聊性能之前,先把一个常见误解清掉。
FlatList 本质只做了三件事:
-
虚拟化
- 控制「哪些 item 被渲染」
- 控制「哪些 item 被回收」
-
scroll 事件桥接
- Native 滚动
- JS 侧接收 offset
-
把 item 组件当黑盒渲染
- FlatList 不关心你 item 里写了什么
- 它只管你什么时候 renderItem
也就是说:
FlatList 不负责你的 state,不负责你的 Context,不负责你的 Redux。
你列表一滑就掉帧,99% 不是 FlatList 算错了可视区域,而是它被迫“重复干活”了。
二、一次点赞操作,为什么能让整个列表卡住?
我们从一个最日常的需求开始。
场景描述
一个 feed 列表,每一项都有一个「点赞」按钮。
列表
├── Item #1 👍
├── Item #2 👍
├── Item #3 👍
...
你点了第 1 条的 👍,结果滑动开始掉帧。
为什么?
三、Demo:一次 state 更新引发的渲染雪崩
问题版本 Demo(可直接跑)
import React, { useState } from 'react';
import { FlatList, Text, TouchableOpacity, View } from 'react-native';
const mockData = Array.from({ length: 100 }, (_, i) => ({
id: String(i),
title: `Item ${i}`,
}));
export default function App() {
const [likedId, setLikedId] = useState<string | null>(null);
return (
<FlatList
data={mockData}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
const liked = item.id === likedId;
return (
<View style={{ padding: 16 }}>
<Text>{item.title}</Text>
<TouchableOpacity onPress={() => setLikedId(item.id)}>
<Text>{liked ? '已点赞' : '点赞'}</Text>
</TouchableOpacity>
</View>
);
}}
/>
);
}
这个 Demo 做错了什么?
从功能角度看,它是完全正确的。
但从渲染模型来看,问题非常致命。
四、渲染触发链路拆解(重点)
当你点击一次点赞,发生了什么?
1. JS 线程:state 更新
setLikedId(item.id);
App组件 state 发生变化- 整个 App 函数重新执行
2. React Reconcile:FlatList 重新 render
renderItem是一个 inline 函数- React 无法判断哪些 item “不受影响”
结果是:
FlatList 会重新调用所有可视区域内的 renderItem
哪怕你只点了第一个 item。
3. UI 线程:layout + draw
- 所有 item 重新生成 element tree
- Yoga 重新算布局
- UI 线程绘制新视图
4. Native 线程:scroll 与 JS 竞争时间片
当你正在滑动时:
- Native 正在处理滚动
- JS 同时在做 diff + render
- Bridge 消息频繁来回
这时候你看到的就是:
滑动开始掉帧,甚至有“卡一下”的感觉。
五、FlatList 真的慢吗?不是,是你让它每次都全算了
FlatList 的虚拟化只解决“不可见 item 不渲染”的问题,它解决不了这两件事:
- renderItem 被重新调用
- item 组件本身被重新 render
也就是说:
FlatList 能保证「不渲染 100 条」,
但保证不了「不重复渲染 10 条」。
六、正确拆法:把状态从列表层“拿走”
改造目标
- 点赞只影响当前 item
- 列表不因为一个点赞而整体 rerender
改造后的 Demo
const Item = React.memo(function Item({
title,
onLike,
}: {
title: string;
onLike: () => void;
}) {
const [liked, setLiked] = useState(false);
return (
<View style={{ padding: 16 }}>
<Text>{title}</Text>
<TouchableOpacity
onPress={() => {
setLiked(true);
onLike();
}}
>
<Text>{liked ? '已点赞' : '点赞'}</Text>
</TouchableOpacity>
</View>
);
});
export default function App() {
return (
<FlatList
data={mockData}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Item
title={item.title}
onLike={() => {
console.log('like', item.id);
}}
/>
)}
/>
);
}
七、这次性能为什么好了?
关键点只有两个:
1. 列表层不再持有 item 状态
App不再有likedId- FlatList 不再因为点赞重跑 renderItem
2. item 变成“局部自洽组件”
- state 在 Item 内部
- React.memo 让未变化 item 直接跳过 render
八、JS / UI / Native 线程协作视角再看一次
现在再看这三条线程:
JS 线程
- 只有一个 Item 的 state 变化
- 没有大规模 diff
UI 线程
- 只有一个 item layout 变化
- 滚动不卡
Native 线程
- scroll 完全独立
- bridge 消息极少
你不是“优化了 FlatList”,而是“停止折磨 FlatList”。
九、跨端视角:为什么 Web / Vue 项目更容易“感觉不到问题”
很多人会说:
Web 里我这么写也没事啊。
原因很简单:
- 浏览器渲染线程够强
- DOM diff 有大量底层优化
- scroll 与 JS 解耦得更彻底
但 RN 把这些问题提前暴露出来了。
这也是为什么:
RN 的性能问题,本质上是在逼你写“更干净的组件边界”。
而这些边界,回到 Web、Vue、甚至原生,都会变成长期收益。
十、总结
如果你只记住一句话:
RN 列表性能问题,80% 不是 FlatList,而是你把“不该放在列表层的状态”放进去了。
真正有效的优化顺序应该是:
- 先拆状态边界
- 再看 renderItem 是否稳定
- 最后才轮到 FlatList 参数
FlatList 很少是第一个需要被“优化”的对象,它更多时候只是最先暴露问题的地方。

1794

被折叠的 条评论
为什么被折叠?



