React Native性能优化全面指南

Viewed 0

一、React Native介绍

React Native 是 Facebook 在 React.js Conf 2015 推出的开源框架,它允许开发者使用 React 和 JavaScript 来调用移动平台的原生 API,从而构建 Android 和 iOS 应用。其核心是通过一个桥接(Bridge)层来实现 JavaScript 与原生代码的通信,开发者主要关注 JavaScript 和 Virtual DOM,底层转换工作由框架完成。

1、RN优势

  1. 开发成本低:使用统一的 HTML/CSS/JS 技术栈,无需关注平台差异。
  2. 跨平台:一次开发可部署到 Android 和 iOS,减少重复工作。
  3. 热更新:支持无需应用商店审核的代码更新。
  4. 可扩展性:易于集成原生模块。
  5. 易学易用:基于 React 和 JavaScript,前端开发者上手快。

2、RN劣势

  1. 成熟度:框架更新较频繁,学习与试错成本较高,部分问题解决方案较少。
  2. 性能:整体性能仍与原生应用存在差距。
  3. 兼容性:底层功能常需针对双平台单独开发。
  4. 第三方库:可用库数量相对有限,某些场景可能受限。
  5. 原生功能:部分高级原生特性需要开发者自行实现。

3、跨平台框架比较

以下从不同维度对比几种开发模式:

开发模式 原生开发 混合开发(如React Native) Web开发
运行环境 Android, iOS 混合App/WebView 浏览器
编程语言 Java, Objective-C等 JavaScript, Dart等 HTML, CSS, JS
可移植性 一般
开发速度 一般
性能 一般
学习成本 一般

混合应用结合了Web的快速开发和原生的良好体验,能有效降低开发成本和时间。

4、React Native 0.68之前的架构问题

旧架构严重依赖JsBridge进行通信,容易引发UI渲染阻塞。性能问题突出的组件包括:

  • ScrollView:一次性渲染所有子项,导致启动慢、内存占用高。
  • FlatList:虽做了组件回收,但快速滑动时易出现白屏和卡顿。
  • 异步渲染:Shadow层到原生UI的渲染是异步的,滑动过快会产生大量UI更新堆积,事件阻塞在JsBridge可能导致长时间白屏。

5、RN中的视图渲染流程

  1. JavaScript线程:执行JS代码,定义UI的结构、样式和属性。
  2. Shadow线程(Layout线程):创建Shadow Tree(类似虚拟DOM),由Yoga引擎将Flexbox等布局样式解析为具体平台的布局信息(宽高、位置)。
  3. 主线程(UI/原生线程):负责最终的原生UI渲染和调用原生模块。

6、RN架构设计——新旧架构对比

新架构针对旧有瓶颈进行了重大革新:

  • JavaScript层:支持React 16+新特性,引入JSI(JavaScript Interface)允许替换JS引擎,并增强了静态类型检查(CodeGen)。
  • Bridge层:拆分为负责UI管理的Fabric和负责原生模块的TurboModules。
  • Native层:推行“LEAN Core”,将非核心模块移出主仓库,交由社区维护。

7、从0.68版本开始的新架构详解

  1. JSI (JavaScript Interface):一个C++轻量级框架,允许JavaScript直接调用原生方法,省去了序列化/反序列化步骤,大大提升了通信性能并支持更换JS引擎。
  2. CodeGen:一个代码生成工具,可将带有静态类型(Flow/TS)的JS代码转换为Fabric和TurboModules所需的原生代码,提前发现类型错误。
  3. 优化Bridge层
    • Fabric:新的同步渲染系统,简化了UI渲染流程。
    • TurboModules:通过JSI实现JS对原生模块的同步、按需调用,减少启动时间。
  4. 精简核心代码 (LEAN Core):将非必要模块从核心包中剥离,由社区独立维护,使核心更轻量。

8、React核心概念回顾

React 遵循“一次学习,随处编写”的理念,采用数据驱动和虚拟DOM。虚拟DOM的好处一是通过Diff算法优化性能,避免直接操作真实DOM;二是提供了跨平台的抽象层,使得同一套逻辑可以适配到网页、移动端等各种渲染终端。其主要构成包括:

  • 组件:类组件与函数式组件。
  • 属性 (Props)状态 (State)
  • JSX:在JavaScript中编写类似HTML的语法。
  • Hook API生命周期(具体内容待补充)。

二、性能优化点

1、RN加载的主要时间消耗

(具体时间消耗分析待补充)

2、拆包与分包

RN的JavaScript代码包通常通过热更新下发。为优化下载体积,应将包拆分为两部分:

  1. Common包:包含React Native框架源码,该部分基本不变,可内置到应用中以减少下载。
  2. 业务代码包:包含业务逻辑,动态下载。
    实现方式:在JSContext中先加载内置的Common包,再加载下载的业务包,从而完整渲染页面。

3、首屏渲染优化

对于庞大的业务代码,可进行二次拆分:将一个业务包拆分为一个主包和多个子包(按模块划分)。进入页面后优先加载主包以快速渲染首屏,当用户与特定模块交互时,再动态加载对应的子包,从而进一步提升加载速度。

4、组件懒加载

使用 React.lazyReact.Suspense 实现组件懒加载,减少初始渲染的节点数,从而提升首屏加载速度并减小打包体积。

import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
        <AnotherComponent />
      </Suspense>
    </div>
  );
}

可以进一步封装以简化使用:

import React, { lazy, Suspense } from 'react';
const OtherComponentLazy = lazy(() => import('./OtherComponent'));
const AnotherComponentLazy = lazy(() => import('./AnotherComponent'));

function Suspensed(LazyComponent) {
  return (props) => (
    <Suspense fallback={<div></div>}>
      <LazyComponent {...props} />
    </Suspense>
  );
}

export const OtherComponent = Suspensed(OtherComponentLazy);
export const AnotherComponent = Suspensed(AnotherComponentLazy);

5、避免重复与无限渲染

render 方法应为纯函数。避免在 render 或提交阶段的生命周期钩子(如 componentDidUpdate)中直接调用 setState,否则可能导致递归更新或重复渲染,阻塞UI更新。类组件应使用 getDerivedStateFromProps,函数组件应在事件处理函数中更新状态。

6、组件卸载前清理操作

在组件中注册的全局事件监听器或定时器,必须在 componentWillUnmount(类组件)或 useEffect 的清理函数(函数组件)中移除,防止内存泄漏和性能影响。

import React, { Component } from 'react';
export default class Hello extends Component {
  componentDidMount() {
    this.timer = setTimeout(() => {
      console.log('定时器执行');
    }, 500);
  }
  componentWillUnmount() {
    this.timer && clearTimeout(this.timer);
  }
}

7、使用Fragment减少视图层级

使用 React.Fragment 或其简写 <> 包装子元素,可以减少不必要的包装 div,从而精简DOM结构,有利于渲染性能。

<>
  <ChildA />
  <ChildB />
</>

8、状态下放,缩小状态影响范围

将状态尽可能下放到使用它的最小组件内部,避免状态提升导致不必要的子组件重渲染。这符合“变的和不变的分开”的原则。

// 优化前:颜色状态在父组件,导致不依赖颜色的ExpensiveTree也重渲染
function App() {
  const [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree /> {/* 不依赖color,但会随App重渲染 */}
    </div>
  );
}

// 优化后:将颜色状态及相关UI抽离到Form组件
function App() {
  return (
    <>
      <Form />
      <ExpensiveTree /> {/* 不再受color状态影响 */}
    </>
  );
}
function Form() {
  const [color, setColor] = useState('red');
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
}

9、使用PureComponent和React.memo跳过不必要更新

  • 类组件:继承 React.PureComponent,它会对 propsstate 进行浅层比较,若无变化则阻止渲染。
  • 函数组件:使用 React.memo 进行包裹,同样进行浅比较。对于引用类型的数据,需提供自定义比较函数作为第二个参数。
import React, { memo } from 'react';
function ShowName({ person }) {
  console.log('render...');
  return <div>{person.name} {person.age}</div>;
}
function compare(prevProps, nextProps) {
  // 仅当name或age改变时才重新渲染
  if (
    prevProps.person.name !== nextProps.person.name ||
    prevProps.person.age !== nextProps.person.age
  ) {
    return false; // 返回false表示props不同,需要渲染
  }
  return true; // 返回true表示props相同,跳过渲染
}
export default memo(ShowName, compare);

10、使用shouldComponentUpdate进行深度控制

在类组件中,可通过 shouldComponentUpdate 方法编写自定义的比较逻辑,实现比浅比较更精细的控制。

shouldComponentUpdate(nextProps, nextState) {
  if (nextProps.someProp !== this.props.someProp) {
    return true;
  }
  return false;
}

11、使用useMemo和useCallback缓存优化

  • useMemo:缓存计算结果,避免在每次渲染时都进行耗时的计算(如大列表统计)。
  • useCallback:缓存函数引用,避免因函数引用变化导致依赖该函数的子组件不必要的重渲染。
import React, { useState, useCallback } from 'react';
function ParentComponent() {
  const [count, setCount] = useState(0);
  const incrementCount = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 依赖count,仅在count变化时创建新函数
  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onIncrement={incrementCount} />
    </div>
  );
}
// 假设ChildComponent是纯组件,被memo包裹
const ChildComponent = React.memo(({ onIncrement }) => {
  return <button onClick={onIncrement}>Increment from Child</button>;
});

12、列表使用唯一的Key

为列表项提供稳定且唯一的 key 属性,帮助React高效识别元素的增删改。

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>{number}</li>
);

13、避免使用内联函数定义

内联函数在每次渲染时都会创建新的函数实例,导致子组件不必要的重渲染。应优先在构造函数中绑定或使用类属性语法(类组件),或使用 useCallback(函数组件)。

// 不推荐:内联函数
<input onClick={(e) => this.setState({ value: e.target.value })} />
// 推荐:预定义方法
handleClick = () => { /* ... */ }
<input onClick={this.handleClick} />

14、避免使用内联样式

内联样式需要JavaScript在运行时为元素添加样式,性能较低。应尽量使用外部CSS或StyleSheet对象。

// 推荐
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
  button: { backgroundColor: 'blue' }
});
<View style={styles.button} />

15、事件节流与防抖

  • 节流 (Throttle):适用于需实时反馈但需限制频率的场景,如窗口调整大小(resize)、页面滚动。可使用 requestAnimationFrame 实现更平滑的节流。
  • 防抖 (Debounce):适用于需等待用户停止操作后再执行的场景,如搜索框输入。它确保只响应用户最后一次输入。
    合理使用这两种技术可以避免因频繁回调导致的性能问题。
0 Answers