ReactNative总结系列三 --- 性能优化

编程这件事,基本绕不开性能优化。这篇文章就结合我实际踩过的坑和经验,把 React Native 里跟性能相关的知识梳理一下。

有关FlatList滚动白屏与渲染卡顿的优化,我放在了我的下一篇文章中单独来说ReactNative总结系列四-FlatList白屏卡顿优化

如何检查性能 - 使用react-devtools

优化之前,得先知道哪里慢。React Native 这边推荐用 React DevTools

低版本 RN 还能用 react-dev-tools 内置的 react-tool,但高版本已经不再维护了,直接用 react-devtools 就好。注意版本要和项目里的 react 版本对应上。

基本用法

  1. 全局安装:yarn global add react-devtoolsnpm install -g react-devtools

    • 临时用 npx react-devtools
    • 我的rn配置用最新版的会报错,使用6.1.2可以
    json 复制代码
    "react": "19.1.4",
    "react-native": "0.81.6",
  2. 终端启动:react-devtools

  3. 打开app,并打开dev menu(rn控制台里按'd'或者摇一摇或者手机里输入'comand+m'),会自动连接上

    如果 DevTools 连不上,用 adb reverse 8097 8097 把 8097 端口转发一下

  4. 这里点击App,点开就能看到 hooks、props、style 等 ,鼠标移上去还会在app里高亮显示

性能分析

  1. 切到 Profiler 面板,点击设置图标,打开录制造成渲染的原因
  2. 点开始录制
    • 在 App 里做你要测的操作,完成后再点一下点结束录制
  3. 此时会生成火焰图,有颜色的部分表示发生了重新渲染(黄色一般说明渲染耗时较长)

3. 鼠标移动到对应的view上,能看到这次渲染是因为哪个 hook 变了、props 变了,还是别的什么原因

  1. 最后需要根据你的预期和实际的渲染次数及view来判断是否合理,比如点了3次,应该刷新3次,却刷新了4次或者5次,或者渲染时,子组件并不需要渲染,但实际渲染了。那就要结合日志分析是哪里造成的多次渲染。

渲染相关的机制

这章介绍一下,React在渲染时的一些机制

require 和 import 在 RN 的行为

  • require:运行时加载,但只会加载一次 ,后续都是直接用运行环境里的缓存。
    • 那么如果你要require的库比较大,比如pinyin库,你放在顶层,就会导致一开始就去加载这个js,导致慢。那么你就可以不写在顶层,用require在实际需要使用时,再导入进来
  • import:分成静态import和动态import
    • 静态import:import {Text} from 'react-native';放顶层 ,和require一样
    • 动态import: const rnModule =await import('react-native'); 代码运行到这时异步加载import里的内容require是同步的

JSX 里的匿名函数

写在 JSX 里的匿名函数,每次渲染都会重新创建,导致memo失效。处理方式:

  • useCallback 包一层
  • 或者把函数提到组件外部

使用Object.assign() 代替 ...

在 TS 或 core-js 里,大量使用 ... 展开运算符可能会有性能损失,使用 Object.assign() 来做对象合并。

异步更新与批量合并

  • 事件触发、useEffect时 多次调用setState,会合并执行,只触发一次渲染
  • rendersetTimeoutPromise异步回调里多次 setState,调用几次setState就重新渲染多少次

这个特别在获取接口数据,返回后设置state时注意

组件结构 > 记忆化 API

记住一个原则:优化组件结构,往往比用 memouseMemo 这些"记忆" API 来得更高效、更重要。

  • 组件层级越深,性能越差。很多优化的本质,其实都是在"减少层级、减少渲染影响的组件"
  • 多使用自定义 Hook 把逻辑封装起来,既方便复用,也让代码看起来更加精简和整洁

Native组件props的更新机制

  • 首先我们要知道一件事,就是**js层重新渲染了,props变了,不一定会触发native层的props更新**
    • 如果你传的是 ReadableMapRN 是会做深比较的

比如js的props每次都生成一个新的obj: {age: 18} ,如果这个age不变,只是obj的引用变了,那么native是不会收到更新的

  • 低版本RN(老架构),从js层更新到native层时,是只要有一个变了,就把所有 props 重新设置一遍
  • 高版本 RN(Fabric 架构)往原生传 props 时,已经能做到变了哪个字段才更新哪个。这一点在升级架构时可以多留意。

key 作用和用法

key 属性是给 React Diff 算法看的,用来在同层级下识别元素是新增、修改还是移除。

  • key不变:组件继续存在,只根据新 props 重渲染。
  • 变了 key:旧组件被卸载,新组件从头创建并挂载。开销大,但能方便地重置组件内部状态。
  • 用 index 做 key 的大坑 :如果你是使用map来同时生成多个一样的组件时,rn会提示你让你都加上key。但是不要使用index来做key(除非数组不变变化
    • 当数组头部插入元素时,新插入的元素里的state会变成原来的第一个元素的state ,也叫内部状态错误。并且所有元素重渲染,最后一个元素还得重新生成组件重新挂载

几个实际场景的表现:

jsx 复制代码
{ids.map(id => (
  // ❌ 不加 key,每次都会重新生成组件
  <MemoItem id={id.toString()} />
))}

{ids.map(id => (
  // ✅ 加上稳定 key,新增/删除项时其他项不会重渲染,只会进行移动
  <MemoItem key={id} id={id.toString()} />
))}

... 
const [items, setItems] = useState([
  { id: 'a', text: 'Apple' },
  { id: 'b', text: 'Banana' },
]);

// 删除第一个元素时...
const removeFirst = () => setItems(prev => prev.slice(1));

// ❌渲染(错误用法),删除后,banana会使用apple里的state不会变!!!
{items.map((item, index) => (
  <Text key={index}>{item.text}</Text>
))}
...
// ⚠️ 但 key 管不了父组件
<MemoParent title={'我也会重新渲染'}>
  <MemoItem key={'我有key'} id={'我有ID'} />
</MemoParent>

几个优化方案

使用memo缓存组件

拆分 Context,告别"连坐式渲染"

很多时候我们只是需要用 Context来更新数据,并不需要使用数据。但数据一变,这些组件也会被动重新渲染。

解决方案:使用多个context

  • 一个 Context 只提供不变的状态更新函数
  • 另一个 Context 提供可变的状态数据

**然后用不变的包裹住可变的,就能把更新视图的范围压到最小。**

这个思路我后来抽了个轻量的共享工具,查看小工具

善用 React 18 的新特性

tsx 复制代码
// 懒加载:只在需要时才加载该组件的代码
const HeavyComponent = lazy(() => import('./HeavyComponent'));

export default function App() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
      <Text>首屏只渲染轻量内容</Text>
      <Button title="加载重型组件" onPress={() => setShowHeavy(true)} />

      {showHeavy && (
        <Suspense fallback={<ActivityIndicator size="large" />}>
          <HeavyComponent />
        </Suspense>
      )}
    </View>
  );
}
  • 使用 useTransitionuseDeferredValue:把不紧急的更新标记为过渡,自动推后优先保证用户交互的流畅感。
tsx 复制代码
const [isPending, startTransition] = useTransition();
...
// 等空闲时才更新,优先保证界面。不过我目前用下来在rn里感受不是很大
startTransition(() => {
        setFilter(text);
      });
...
{isPending && <Text>正在更新...</Text>}
...

 const [text, setText] = useState('');
 const deferredText = useDeferredValue(text); // 延迟更新的值,界面上直接用这个延迟值
 ...
 <Text>{deferredText}</Text>
 ...

可见时才加载组件

  • 在长滚动页面里,对于那些非首屏可见的组件,可以用 IntersectionObserver 做"可见性触发"

    • 一开始先放个占位组件,等 isIntersecting 回调时再渲染真正的组件或者加载图片等
  • 既能减少首屏压力,又能降低运行时的渲染负担。

相关推荐
fengxin_rou3 小时前
Java垃圾回收机制深度解析:从原理到实战
java·jvm·性能优化·gc·垃圾回收
JohnnyDeng9416 小时前
【Android】Room 数据库高级用法与性能调优:从查询瓶颈到毫秒级响应
android·性能优化·kotlin·room
kyriewen17 小时前
前端性能优化:LCP 从 4s 到 0.9s 的 5 个核心手段(附配置代码)
前端·javascript·性能优化
英俊潇洒美少年19 小时前
前端性能优化:非关键脚本/第三方资源异步加载全解(彻底解决首屏阻塞)
前端·性能优化
leeyi19 小时前
Graph 编排:不只是 ReAct 的通用 DAG
react native·agent·graphql
真实的菜20 小时前
Redis 从入门到精通(十三):性能优化与运维实战 —— 慢查询、内存优化、监控与安全
运维·redis·性能优化
我最爱吃鱼香茄子20 小时前
终极方案:JetBrains IDE永久解放C盘空间
计算机视觉·性能优化·电脑·笔记本电脑·intellij-idea·程序员创富·webstorm
java_cj1 天前
Elasticsearch索引管理完全指南:从基础API到ILM生命周期管理
大数据·后端·elasticsearch·性能优化
不爱吃糖的程序媛1 天前
React Native 三方库 react-native-version-number 鸿蒙适配实战:从零到版本信息展示
react native·react.js·harmonyos