信息流中有 20 个项目感觉非常完美。滚动起来如丝般顺滑。你的客户很满意。然后你将其发布到生产环境,数据传了过来,突然间你面对的是两千行数据。UI 开始卡顿。内存不断攀升,直到操作系统强制关闭应用。在绝望中,一些开发者会将所有内容都包裹在一个 ScrollView 中,然后了事。这种决定通常每解决一个问题,就会衍生出三个新 Bug。
列表是决定用户如何感知你的 React Native 应用的性能瓶颈。处理得当,应用感觉就像原生应用一样;处理不当,即使是最精美的界面也会变得极其迟钝。根本原因通常是你选择的组件与你要求 JavaScript 线程和 UI 线程执行的工作之间不匹配。React Native 在两条轨道上运行。你的逻辑运行在 JS 线程上,而渲染则发生在原生 UI 线程上。当你错误地渲染一个庞大的列表时,两个线程都会淹没在布局计算、重新渲染和内存分配中。结果就是掉帧、白屏闪烁,并最终导致崩溃。
Pick the Right Tool
选择列表组件应该是一个深思熟虑的架构决策,而不是一种本能反应。
ScrollView 是最简单的选择。它会接收你给它的每一个子组件,立即将每一个都挂载到内存中,并将这一大堆内容交给原生滚动引擎。这正是你处理短小、固定内容时所需要的,例如设置界面、登录表单,或者只有十个部分的静态产品详情页。它的行为是可预测的,且易于设置样式。问题在于它没有虚拟化机制。如果你给它两千个项目,它会听话地创建两千个原生视图。不要将 ScrollView 用于大型或动态数据集。把它想象成一个装裱好的海报,而不是一个图书馆的书架。
FlatList 是处理长而统一信息流的得力助手。它实现了内容虚拟化,这意味着它只挂载当前可见或靠近视口的行。随着用户的滚动,FlatList 会卸载离开屏幕的单元格,并将其回收以供新数据使用。无论你的数组变得多大,这都能保持内存占用平稳。如果你正在构建社交时间线、通知中心或任何持续滚动的类似卡片集合,FlatList 是默认的正确选择。
SectionList 是具有组织感的 FlatList。当你的数据以分组形式到达时,请使用它,例如按字母顺序排列的通讯录、按日期划分的健身日志,或按月份组织的账单列表。它会渲染粘性分组标题,并为你处理分组逻辑。其底层使用了与 FlatList 相同的虚拟化引擎,因此你可以在获得带标题分区结构的同时,享受相同的内存优势。
FlashList 当你需要榨干设备的每一帧性能时,它便派上用场了。它构建在 RecyclerListView 生态系统之上,比 FlatList 更激进地回收视图,旨在即使在中端硬件上也能保持每秒 60 帧。如果你正在构建高吞吐量的聊天界面、滚动速度极快的商品目录,或者任何流畅度即竞争优势的界面,那么增加 FlashList 这个依赖是值得的。它并非每个界面都必须使用,但对于定义核心体验的信息流来说,其性能差异是显而易见的。
Common Performance Killers
当列表开始变得迟钝时,通常有三个“嫌疑人”。
一次性挂载过多的 React 树是最严重的失败。当每一行都是一个复杂的组件树时,初始渲染可能会长时间阻塞 JS 线程,导致出现白屏或难看的首次绘制延迟。用户打开应用后只能等待。即使在初始加载之后,沉重的行也会使滚动初始化变得迟缓,因为前几帧会被初始化工作消耗掉。
每帧工作量过大表现为滚动时的卡顿。为了保持动画流畅,你每帧大约只有 16 毫秒的预算。如果行组件在 render 过程中执行昂贵的计算、即时解析日期或进行深层对象比较,你就会超出这个预算。UI 线程会掉帧,用户会感到明显的顿挫感。
过度使用内存是隐形杀手。每一个原生视图都会消耗 RAM。如果再加上未经优化的超大图片、每个卡片上的阴影或嵌套的可点击组件,内存占用会成倍增加。在 iOS 上,系统可能会在不发出警告的情况下终止你的应用;而在 Android 上,用户会眼睁睁看着延迟不断累积,直到应用变得无法使用。
Optimization Checklist
Small strategic habits separate a list that merely works from one that flies.
Use stable keys. Always pass a real identifier from your data set to the key prop. Never use the array index. If your list reorders, filters, or appends items, an index-based key tricks React into pairing the wrong data with the wrong recycled component. That mistake triggers unnecessary unmounts, state mismatches, and cascading re-renders. A proper ID tells React exactly which row moved where.
Memoize rows. Wrap your row component in React.memo so that it only re-renders when its props actually change. Without this guard, any parent state update can trigger a render pass across every visible row, even if their data is identical. In a long list that churns, those wasted cycles add up fast.
Keep renderItem stable. Avoid defining a new function directly inside the renderItem prop on every parent render. An inline arrow function like renderItem={({ item }) => <Row data={item} />} creates a new reference each time the parent updates. FlatList sees a changed prop and recycles the row unnecessarily. Define the render function outside the component or memoize it with useCallback so the reference stays stable.
Use getItemLayout whenever possible. If your rows have a fixed or predictable height, tell FlatList exactly what it is. This prop allows the list to skip expensive native measurement calls. Instead of measuring each cell after mount, the list calculates position mathematically. The difference is especially sharp on lists with hundreds or thousands of items, where onLayout chatter can bring the JS thread to its knees.
Optimize images aggressively. Unbounded images are list poison. Always set explicit width and height so the native layer reserves space before the image decodes. For remote images, use a caching library such as Expo Image or an equivalent that handles memory caching, disk persistence, and format optimization. The default React Native Image component works for prototypes, but production feeds need more control over memory and loading states.
Avoid nesting scroll containers. Never place a vertical FlatList inside a vertical ScrollView. The parent ScrollView captures all scroll events and disrupts the child FlatList's ability to measure its viewport. Virtualization breaks because FlatList no longer knows which rows should be visible. The result is that every row mounts anyway, defeating the entire purpose of virtualization. If you need a header above a list, use FlatList's own ListHeaderComponent prop. If you need complex sticky behavior, use a SectionList or FlashList with the appropriate header configuration.
The Golden Rule
If the content is small and finite, let ScrollView handle it. If the content grows with user-generated data or remote pagination, use a virtualized list. When the list is the centerpiece of the app and users will scroll for minutes at a time, reach for FlashList.
Here is one last truth that is easy to forget. Boring rows scroll fast. The lighter your row component, the smoother your list. Strip away nested navigations, heavy computations, and gratuitous animations from the individual row. Keep the markup flat, the logic thin, and the images sized. A list lives or dies by the cumulative weight of what it renders. Make each row cheap, and the list will feel expensive in the best possible way.
