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)

四个方面一起进行优化。

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

相关推荐
恋猫de小郭6 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅12 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅13 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅14 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅14 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊14 小时前
jwt介绍
前端