CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
React Native 开发入门/课时/性能优化
RawBlame
课时目录12 节

React Native 开发入门

  1. 01RN 与 Expo
  2. 02核心组件
  3. 03布局与列表
  4. 04平台差异
  5. 05导航与手势
  6. 06State 与 JSX
  7. 07网络
  8. 08本地存储
  9. 09原生模块
  10. 10动画
  11. 11性能优化
  12. 12测试与发布

性能优化

FlatList/Metro/Profiling。

30 分钟

性能优化

这一课你将能做到

  1. 正确区分 memo / useMemo / useCallback 的职责,消除不必要的组件重渲染
  2. 调优 FlatList 参数,知道什么场景该上 FlashList,让万级列表滚动不掉帧
  3. 优化图片加载和 App 启动速度,建立「先测量再优化」的科学方法论

为什么要学

功能做完只是及格线。用户用的不是你的 MacBook Pro,而是三年前充话费送的中端安卓机——列表一卡、图片一白,用户三秒后就划走了。性能优化是区分「能跑的代码」和「能上线的产品」的分水岭,也是技术面试里最高频的区域之一。

但优化最怕的是「盲目动手」。很多人把整棵组件树都包上 memo,结果比较 props 的开销反而超过了重渲染本身;或者还没定位瓶颈就把 FlatList 的参数全调一遍。性能优化的第一课是测量,第二课才是动手。

人话:性能优化的核心就一句话——少做事。能不渲染就不渲染(memo 跳过重渲染),能不重算就不重算(useMemo 缓存计算结果),能让原生做就让原生做(FlashList 回收复用、expo-image 的磁盘缓存、Hermes 预编译)。JS 线程是单线程,它一忙所有交互和动画都得排队。

先建立图像

一个重渲染失控的典型场景,这也是本课所有优化手段的出发点:

父组件 state 变了(比如搜索框打一个字)
  → 父组件重新渲染
    → 所有子组件跟着重新渲染(即使 props 没变)
      → 子组件的列表项全部重画
        → 100 行列表 → 掉帧卡顿

解决思路是在「父变不影响子」的地方加隔断:

父组件 state 变了
  → 父重新渲染
    → 子组件(包了一层 memo)发现 props 没变 → 跳过渲染
      → 只有真正相关的组件重画 → 流畅

另外一张图:列表滚动的内存视角。

FlatList 的「窗口化」策略(默认):
可视区(1 屏)     ← 活跃渲染
上一屏 + 下一屏   ← 保留但不渲染(windowSize 控制)
更远的行           ← 组件实例销毁,下次滚回来重新创建

FlashList 的「回收复用」策略:
可视区            ← 活跃渲染
已滚出的行         ← 组件实例被复用,只换 props 和数据
                    不销毁、不重新创建 → 内存和帧率双优

跟着做

步骤 1:memo 阻止无效重渲染

import React, { useState } from 'react';
import { Text, TextInput, View } from 'react-native';

// 包一层 React.memo:props 不变就不重渲染
const ExpensiveRow = React.memo(({ title }) => {
  console.log('Row 渲染了:', title);
  return <Text style={{ padding: 16 }}>{title}</Text>;
});

export default function App() {
  const [text, setText] = useState('');

  return (
    <View style={{ flex: 1, paddingTop: 60 }}>
      <TextInput
        value={text}
        onChangeText={setText}
        placeholder="打字试试"
        style={{ borderWidth: 1, padding: 12, margin: 16 }}
      />
      <ExpensiveRow title="我不受输入影响" />
    </View>
  );
}

不加 memo,每打一个字控制台都会输出一次「Row 渲染了」;加了 memo,Row 只在 title 变化时才渲染。打开控制台亲眼验证——这是理解 memo 最快的方式。

注意 React.memo 做的是浅比较:props 里的基本值(string/number/boolean)没问题,对象和数组只要引用没变就通过,引用一变就失效——所以内联对象会穿破 memo。

步骤 2:useCallback 固定函数引用

如果 memo 的子组件接收了一个回调函数,而父组件每次渲染都写一个内联函数,函数引用每轮都变,memo 白做:

// 错:每次渲染 handlePress 都是新函数
<ExpensiveRow onPress={() => handlePress(id)} />

// 对:用 useCallback 固定引用
const handlePress = useCallback((id) => {
  navigation.navigate('Detail', { id });
}, [navigation]);

<ExpensiveRow onPress={handlePress} />

useCallback 的依赖数组 [navigation] 表示:navigation 不变,handlePress 就不变。空数组 [] 表示「初始化一次,永不再变」。

重要:别给所有函数都套 useCallback。函数创建成本极低,useCallback 本身也有依赖比较开销。只在两种场景用:①传给 memo 子组件的回调;②作为其他 useEffect / useMemo 的依赖项。

步骤 3:useMemo 缓存计算结果

const filteredList = useMemo(
  () => list.filter(item => item.name.includes(keyword)),
  [list, keyword]
);

假设 list 有 1 万条,每次父组件 state 变化(比如切换标签页)都会重新 filter——用 useMemo 包裹后,只要 list 和 keyword 没变,直接复用上一次的计算结果。

同样规则:只有计算开销大(过滤、排序、大对象映射)且依赖变化不频繁时,才值得上 useMemo。

步骤 4:FlatList 性能参数调优

FlatList 自带的几个参数,调好了性能提升显著:

<FlatList
  data={list}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}    // key 必须稳,不要用 index
  initialNumToRender={10}              // 首屏渲染行数,控制启动时的渲染压力
  maxToRenderPerBatch={10}             // 每批增量渲染行数,滚动时逐批加载
  windowSize={5}                       // 保留几屏内容(5 屏),默认 21(太大)
  removeClippedSubviews                // 卸载屏外视图,Android 收益大
  getItemLayout={(_, i) => ({ length: 72, offset: 72 * i, index: i })} // 行高固定必给
/>

逐条解释:

  • keyExtractor 不能用数组索引:删了中间一行之后索引变了,RN 会复用错误的行组件。
  • getItemLayout 是 FlatList 性能的灵魂参数。如果你知道每一行高度固定(比如 72dp),告诉 FlatList 它可以跳过 onLayout 测量,直接跳到对应位置——长列表快速滚到第 500 项时差异巨大。
  • windowSize 默认 21(21 屏),保持大量屏外组件会吃掉大量内存。调小到 3~5 够用。
  • removeClippedSubviews 卸载屏外组件释放内存,Android 效果显著;但遇到绝对定位或 zIndex 复杂的行,可能被误裁剪,出问题先排查这个。

步骤 5:FlashList——Shopify 的终极方案

当数据量过万、FlatList 调参也救不了时,换 FlashList。它的核心创新是「回收复用单元格」——滚出屏幕的行组件不销毁,只换数据再用,类似 iOS UITableView 的 dequeueReusableCell。

npx expo install @shopify/flash-list
import { FlashList } from '@shopify/flash-list';

<FlashList
  data={list}
  renderItem={renderItem}
  estimatedItemSize={72}   // 给一个估算行高就行,不像 getItemLayout 要求精确
/>

API 和 FlatList 几乎一样,但万级数据下滚动帧率稳定 60fps。迁移成本极低:改 import、把 windowSize/getItemLayout/removeClippedSubviews 去掉,加上 estimatedItemSize。

什么时候不换 FlashList:

  • 数据量小于 200 条:FlatList 足够
  • 行内有复杂的绝对定位或 zIndex 重叠:FlashList 的回收复用可能破坏布局

步骤 6:图片与启动优化

图片是 RN 的性能黑洞——原图 5MB 不做处理直接丢进列表,低端机几秒后就 OOM 崩溃。

图片优化三板斧:

  1. 用 expo-image 替代自带 Image:自带磁盘缓存、WebP 支持、过渡动画,内存管理比原生 Image 好很多。
import { Image } from 'expo-image';

<Image
  source={{ uri }}
  style={{ width: 100, height: 100 }}
  contentFit="cover"
  transition={200}          // 淡入过渡
  cachePolicy="memory-disk" // 默认就是,显式声明更清晰
/>
  1. 列表里永远用缩略图:后端提供多尺寸图片,?w=200 或 thumb_ 前缀。原图 4K 放列表里毫无意义。
  2. 图片懒加载:FlatList 配合 expo-image 默认只加载可视区的图片,但初始渲染时如果一次性挂载几百张,仍要控制 initialNumToRender。

启动优化三板斧:

  1. 延迟隐藏启动屏:在 App 加载关键数据期间保持启动屏显示,数据就绪再消失。
import * as SplashScreen from 'expo-splash-screen';

SplashScreen.preventAutoHideAsync(); // App 启动时先阻止自动隐藏

// 数据加载完后
SplashScreen.hideAsync();
  1. 首屏以外的库延迟加载:用动态 import 或把非首屏页面放到二级路由再 import,减小 JS bundle 体积。
const HeavyScreen = React.lazy(() => import('./HeavyScreen'));
  1. Hermes 引擎(Expo 默认已开):把 JS 编译成字节码而非源码运行,启动时间缩短 30%~50%。不需要你做任何配置,但要知道它在那里。

步骤 7:InteractionManager——把重活移出交互响应

用户点击按钮后,别立刻执行重计算。用 InteractionManager 把重活排到所有交互和动画结束后:

import { InteractionManager } from 'react-native';

const onPress = () => {
  navigation.navigate('Result');
  InteractionManager.runAfterInteractions(() => {
    // 大 JSON 解析、复杂数据处理放在这里
    const result = heavyComputation(data);
    setResult(result);
  });
};

这样跳页动画先流畅完成,数据计算在后台慢慢算,用户不会觉得卡。

讲清楚要点

  1. 优化三件套的分工:memo 管组件是否重渲染,useMemo 管计算值是否重算,useCallback 管函数引用是否稳定。三者都是「引用稳定」思维——只要依赖没变,就复用上一次的产物。
  2. 别到处乱包 memo:React.memo 本身有浅比较开销,给所有组件都包一层,比较成本可能超过重渲染。优先包「渲染成本高 + 父组件频繁更新 + 自身 props 很少变」的组件——先测量,再优化。
  3. FlatList 优化的第一原则:renderItem 轻量(别在 renderItem 里做复杂计算)、key 稳定(不能用 index)、行高固定给 getItemLayout。这三条做到,90% 的列表性能问题已经解决。
  4. 图片是性能重灾区:原图直接加载必卡。三招并用:后端出缩略图、expo-image 缓存、列表懒加载。把图片视为网络带宽 + CPU 解码 + 内存占用 + GPU 渲染的四重压力来源。
  5. JS 线程是单线程:大 JSON 解析、复杂数据处理、同步循环会卡住所有交互。要么分批处理(大数组 slice 后逐帧算),要么用 InteractionManager 推到交互之后。
  6. 先测量再优化:用 React DevTools Profiler 看组件渲染次数;Android 开启 GPU 渲染分析看每一帧的绘制时间;先找到真正的瓶颈再动手。不要「感觉卡就全包 memo」,那属于瞎优化。

你可能会懵的地方

  • 用了 memo 为什么子组件还在重渲染? 用 React DevTools Profiler 查看,十有八九是因为 props 里有内联对象、数组或函数。例如父组件传 style={{ marginTop: 10 }} 或 onPress={() => ...},每次渲染引用都变,memo 比较失败。解决:不变的数据提到组件外、函数用 useCallback 固定、样式用 StyleSheet.create 预定义。

  • 怎么判断哪里卡? 按优先级排查:① React DevTools Profiler 看哪个组件渲染最频繁;② Android 开启「开发者选项 → GPU 渲染分析 → 条形图」看每帧是否超过 16ms(绿线);③ 真机连 Flipper 或 Chrome DevTools 看 JS 执行耗时;④ 主观感受——白块、掉帧、点击无反应分别对应不同的根因。

  • FlashList 能完全替代 FlatList 吗? 99% 场景可以,API 几乎一致。但以下情况要注意:① 行内有复杂的绝对定位或 zIndex(回收复用可能破坏布局);② 依赖 FlatList 的 onViewableItemsChanged 做精确可见项逻辑(FlashList 的行为略有不同);③ 需要 ListHeaderComponent 撑满全宽时,FlashList 的回收逻辑可能导致 header 异常。遇到异常先切回 FlatList 排查。

  • removeClippedSubviews 要不要开? Android 开,内存收益明显;iOS 默认行为已经不错,开不开都行。但行内有绝对定位元素时,这个属性可能把绝对定位内容误裁掉,出问题先关它。

  • useMemo 的依赖数组里对象引用一直变怎么办? 用稳定标识(如 id 字符串)替代整个对象作为依赖,或在 useMemo 内部通过 id 去查对象,不要直接把对象本身当依赖。

错法对照

错法问题正解
memo 子组件接收内联函数函数引用每轮都变,memo 失效useCallback 固定引用
无差别给所有组件包 memo比较成本反噬性能只包「渲染昂贵 + 频繁被父更新」的组件
万级列表用 FlatList 裸奔滚动掉帧、白块、内存上涨调 FlatList 参数或换 FlashList
列表里直接加载原图内存爆炸 → OOM 崩溃后端出缩略图 + expo-image 缓存
首屏 import 所有页面组件JS bundle 大 → 启动慢数秒路由级懒加载,非首屏动态 import
FlatList keyExtractor 用 index删行后索引错位,复用错误行用稳定唯一标识,如 item.id
重计算直接阻塞交互响应点击后卡死无反馈InteractionManager.runAfterInteractions
盲目优化不测量优化了不该优化的地方先用 Profiler / GPU 分析找瓶颈

卡点急救

  1. 列表滚动白块:减小 windowSize(3~5)换内存;简化行布局(减少嵌套层级);换 FlashList;检查 removeClippedSubviews 是否误裁了绝对定位内容。
  2. Android 卡 iOS 流畅:Android 中端机 JS 线程性能弱是常态。检查:① 是否有同步重计算(大 JSON 解析、复杂循环);② Hermes 是否开启(Expo 默认开);③ 是否有大量 console.log 在生产环境(RN 的 console.log 是同步的,会拖慢 JS 线程)。
  3. 内存持续上涨:排查三个重灾区——① 图片缓存未清理(expo-image 默认有上限);② 定时器/事件监听器未在组件卸载时清理;③ 闭包持有大对象导致 GC 回收不了(用 WeakRef 或主动置 null)。
  4. 启动时间长达数秒:用 Flipper / Xcode Instruments 的 Time Profiler 看启动耗时大头;优先减少首屏 import 的模块数;延迟加载非必要依赖;确认 Hermes 已启用。
  5. FlatList 快速滚动时图片还没加载就划走了:这是正常现象,expo-image 的缓存会让下次滚回来时秒加载。如果视觉上不能忍,给图片占位色或骨架屏。

面试常问

  • memo / useMemo / useCallback 的区别与配合? memo 是组件级优化:包裹组件后,props 浅比较不变则跳过渲染。useMemo 缓存计算值:依赖没变直接返回上次结果。useCallback 缓存函数引用:保证传给子组件的函数引用稳定,让子组件的 memo 生效。三者都是「引用稳定」思维,常配合使用——useMemo/useCallback 稳定传给 memo 子组件的 props。

  • FlatList 和 FlashList 的本质区别? FlatList 用「窗口化」策略:只渲染可视区 + 少量屏外内容,但滚出屏幕的行组件被销毁,滚回来要重新创建。FlashList 用「回收复用」策略:滚出屏幕的行组件实例被保留、改数据再用(类似 iOS UITableView 的 dequeueReusableCell),极大减少组件创建开销,万级数据下帧率和内存都更优。API 几乎一致,迁移成本低。

  • RN 启动优化有哪些手段? ① Hermes 引擎(字节码预编译);② 减少首屏 JS bundle 体积(路由级懒加载、按需 require);③ SplashScreen.preventAutoHideAsync 延迟隐藏启动屏,掩盖初始化耗时;④ 减少启动时的同步存储读取和原生模块初始化;⑤ 图片、字体等资源预加载后按需释放。

自测三问

  1. 为什么 memo 的子组件收到内联函数会失效?正确的写法是什么?
  2. FlashList 比 FlatList 快的本质原因是什么?它有什么适用边界?
  3. 列表里图片应该怎么处理才能避免 OOM?

小结一句话

性能优化 = 引用稳定(memo / useMemo / useCallback) + 列表回收(FlashList) + 图片缓存 + 启动瘦身 + 重活异步化——先测量找瓶颈,再动手,别瞎优化。

延伸

  • React Native 性能官方指南
  • FlashList 文档
  • expo-image 文档
  • 下一课:测试与发布——用 Jest 保质量、EAS Build 云端出包、TestFlight 内测、OTA 热更新。
加载本节练习…
← 上一课动画下一课 →测试与发布