RN 性能优化:列表滚动掉帧、卡顿怎么办?

@[toc]

前言

只要你做过 RN App,基本都遇到过这种画面:

页面上一个长列表,图片多、文字多、而且还伴随一些交互。滑一滑就掉帧,列表像在"喘气",甚至会白屏闪烁一下。

但其实 RN 的列表不是完全做不到丝滑,只是你必须理解它为什么慢、慢在哪里、哪些优化是有效的。

这篇文章就从纯 JS 层、虚拟列表机制、到原生渲染、图片加载,全链路讲透。

长列表为什么会掉帧

要搞清楚如何优化,先说为什么会掉帧。RN 的列表之所以卡,通常来自:

  1. JS 线程压力大

    • 每一次滚动都会触发 onScroll,JS 线程会堆积大量任务。
    • 你的 renderItem 写得复杂,就会出现频繁的组件重渲染。
  2. 没有正确虚拟化

    • FlatList 默认 windowSize = 21(前10 + 当前1 + 后10),屏幕渲染太多不可见 Cell。
    • 超出屏幕的 cell 仍然挂在 React tree 中,导致 diff 时间变长。
  3. Cell 组件未做 memo

    • 每次父组件 state 更新,会导致所有 cell 重新 render。
  4. 图片加载阻塞

    • 未缓存、未用占位图,滚动中反复解码图片,卡顿明显。
  5. 大数组操作(百万级数据)

    • 大数据传给 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 的核心优势

  1. 强制虚拟化(不让你绕过)
  2. 数据模型基于原生 Recycler 思路(而不是 React 组件)
  3. 支持无限列表
  4. 可以缓存测量结果
  5. 可以绑定 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 万行的日志浏览页面,解决方案是:

  1. 首屏加载 100 行
  2. 分批 append 数据(每次 1000)
  3. 使用 RecyclerListView
  4. 所有 cell 使用固定高度
  5. 文本提前截断
  6. 禁止滚动回到顶部(否则会触发大量重新计算)

滚动体验非常稳定,只要你控制住 JS 线程的任务量,RN 也能应对百万级数据。

SectionList、FlatList、RecyclerListView:到底应该用哪个?

这是团队争论最多的问题,这里给一套能落地的决策矩阵:

场景 推荐方案 原因
简单固定高度列表 FlatList + getItemLayout 简单、够用
图片较多、布局固定 FlatList + windowSize + memo 配置好性能不错
大量复杂图文混排 RecyclerListView 原生级滑动体验
分组列表(Section) SectionList / FlashList 分组能力更强
10w 行以上 RecyclerListView RN 官方虚拟化不够强

总结

RN 列表的性能优化不是靠"加一个参数"能解决,你需要从:

  • 虚拟化(windowSize / getItemLayout)
  • 组件结构(memo / useCallback)
  • 图片加载(缓存 / 占位图)
  • 引擎(RecyclerListView)

四个方面一起进行优化。

只要方法用对,长列表一样能实现接近原生的体验。

相关推荐
京东零售技术6 小时前
2025京东零售技术年度精选 | 技术干货篇(内含福利)
前端·javascript·后端
悦E佳7 小时前
资源&问题链接
前端
布列瑟农的星空7 小时前
2025年度总结——认真生活,快乐工作
前端·后端
点亮一颗LED(从入门到放弃)7 小时前
设备模型(10)
linux·服务器·前端
xingzhemengyou17 小时前
Python 有哪些定时器
前端·python
木西7 小时前
Gemini 3 最新版!Node.js 代理调用教程
前端·node.js·gemini
婷婷婷婷7 小时前
表格组件封装详解(含完整代码)
前端
晴虹7 小时前
lecen:一个更好的开源可视化系统搭建项目--页面设计器(表单设计器)--全低代码|所见即所得|利用可视化设计器构建你的应用系统-做一个懂你的人
前端·后端·低代码
小皮虾7 小时前
这应该是前端转后端最简单的办法了,不买服务器、不配 Nginx,也能写服务端接口,腾讯云云函数全栈实践
前端·javascript·全栈
码途进化论7 小时前
Vue3 防重复点击指令 - clickOnce
前端·javascript·vue.js