React Native 开发中遇到的性能问题分析与优化

React Native 是一款跨平台的移动应用开发框架,能够让开发者用 JavaScript 开发出类似于原生应用的用户体验。然而,由于框架底层的设计和实现机制,React Native 开发中有时会遇到性能问题。本文将在深入分析 React Native 开发中出现的性能问题的原因的基础上,给出性能优化的方法和实践经验。

性能问题的表现和原因

在开发 React Native 应用时,常见的性能问题主要有以下几种表现形式:

  1. 首次渲染过慢:组件初次渲染加载的时间过长;
  2. 卡顿:操作时页面反应迟钝,出现卡顿现象;
  3. 内存占用过高:程序运行一段时间后,内存占用过高,导致程序崩溃或者运行缓慢;
  4. 视图绘制闪烁:页面中视图重复渲染,导致页面绘制闪烁。

造成上述问题的原因大致如下:

  1. JS 线程阻塞:React Native 应用运行在单线程模式下,当 JS 线程执行任务耗时较长时,会阻塞渲染线程,导致页面卡顿;
  2. 内存泄漏:内存一直增长,最终耗尽导致崩溃;
  3. 视图重复渲染:部分组件没有开启 PureRender 优化,每次 render 都会导致视图重 绘和 layout 计算,导致卡顿、闪烁。

那么,如何有效地解决这些问题呢?

性能优化方法

在进行性能优化的时候,每个方案都有自己的利弊,开发者应该结合实际场景选择合适的方案。

减少渲染次数

React Native 有着与 React Web 一样的优点:组件可复用,只有在 props 或 state 发生改变时才会渲染。当组件更新后,React 会通过 setState 或 props 变化等机制重新计算 Virtual DOM 并与之前 Virtual DOM 做一次比对,决定是否进行 Diff 操作。

通过 shouldComponentUpdate 进行手动控制,可以有效地减少不必要的真实 DOM 渲染和 layout 计算,进而避免组件的重复渲染、卡顿和闪烁问题。

比如:

class Component extends React.Component {
  shouldComponentUpdate(nextProps) {
    if (nextProps.value !== this.props.value) {
      return true;
    }
    // 如果其他属性不是必须重新渲染,可以返回 false
    return false;
  }

  render() {
    ...
  }
}

不过,需要注意的是,在使用 shouldComponentUpdate 时,要保证其准确判断 props 和 state 是否发生了改变。如果有一个类似于对象、数组等的引用类型的 prop 正在传递到组件中,那么每一次传递的引用地址都会发生改变,导致所有 shouldComponentUpdate 返回 true,从而影响应用性能。解决方法是可以使用 deepCompare 函数进行深度比较,控制是否更新组件。

合理分离业务逻辑

尽可能避免前端性能调优中常见的混杂代码问题。把 JS 代码分解成可重用的模块,提高模块内部分离度。通俗点说,就是避免一些无关紧要的代码混杂在业务逻辑代码周围,让业务逻辑代码更加纯粹。

减少冗余计算

React Native 中的渲染靠 Virtual DOM,用 JS 的计算代替了 DOM 操作,但如果所需要的计算量过大的话,大量的计算耗费 CPU 资源,会导致页面卡顿。减少计算量或者将计算放在后台线程中处理,能有效地增加应用的性能。在一些场景中,使用缓存技术可以达到优化的效果,例如使用 immutable.js 做数据缓存,避免相同数据的重复计算。

合理使用动画

动画作为 React Native 中常用的用户交互方式,不仅能够夸平台展现出统一的动态效果,而且不需要手写 CSS 或者使用第三方库。但是频繁的使用动画,就会消耗大量的内存和 CPU 资源。因此在使用动画时,建议使用 LayoutAnimation 进行自动布局动画;避免在高频组件(如列表)中使用 slide 等复杂动画,防止卡顿。

import React, {Component} from 'react';
import {LayoutAnimation, Text, TouchableOpacity, View} from 'react-native';

class LayoutAnimationView extends Component {
  state = {
    w: 100,
    h: 100,
  };

  onPress = () => {
    LayoutAnimation.spring();
    this.setState({
      w: this.state.w + 15,
      h: this.state.h + 15,
    });
  };

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity onPress={this.onPress}>
          <View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    backgroundColor: '#f00',
  },
});

优化图片加载

图片资源是 React Native 开发中的重要资源,能够优化图片的加载方式,能有效地提高应用的渲染速度。图片优化如下:

  • 预加载图片,并对图片进行缓存;
  • 对加载过的图片,尽量使用内存缓存,并将数据保存到 AsyncStorage 或本地存储中,下次进入应用时从本地存储读取数据;
  • 对于不同分辨率(@2x、@3x)的图片,加载合适尺寸的图片。

避免隐式布局

在使用 React Native 开发时,尤其是在处理强制布局时,经常会发生隐式布局。隐式布局发生的情况包括:组件尺寸发生改变时没有重新计算布局、使用 setState 时嵌套设置多个 property、在无法用 flexDirection 和 alignItems 解决的问题中使用 justifyContent。

比如下面这种场景:

class Wrapper extends Component {
  state = {
    page: 1,
  };

  onPress = () => {
    LayoutAnimation.spring();
    this.setState({
      page: this.state.page + 1,
      height: newHeight,
    });
  };

  render() {
    return (
      <View style={styles.container}>
        <FlatList data={someData} />
        <View style={styles.footer}>
          <TouchableOpacity onPress={this.onPress}>
            <Text>{`第 ${this.state.page} 页`}</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  footer: {
    alignItems: 'center',
    justifyContent: 'center',
  },
});

当点击 Text,组件高度增加后,由于 FlatList 的样式定位依赖于父容器的高度,所以会重绘整个 FlatList。这种情况下,可以将 FlatList 和仅在组件高度变化时修改的组件单独放到一个 View 中,增加性能。

render() {
  return (
    <View style={styles.container}>
      <View style={styles.flatListWrapper}>
        <FlatList data={someData} />
      </View>
      <View style={styles.footer}>
        <TouchableOpacity onPress={this.onPress}>
          <Text>{`第 ${this.state.page} 页`}</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  flatListWrapper: {},
  footer: {
    alignItems: 'center',
    justifyContent: 'center',
  },
});

应用打包优化

应用打包也是影响性能的一个重要因素,常常会影响应用的启动时间和响应速度。

Code Splitting

代码分片是指将应用代码分成小块,以更快地加载和渲染。通常,加载大量 JavaScript 代码可能会导致应用程序的启动时间变慢。将代码分为 /js 和 /native 两个导入路径,让负载在运行时降到最低限度。

import HomeScreen from './src/HomeScreenComponent';

const AppContainer = createAppContainer(
  createStackNavigator({
    Home: { screen: HomeScreen },
    Details: { screen: DetailsScreen },
  })
);

编译优化

除了代码分割,还可以在打包时使用 minification 和 gzip 压缩来减少文件大小。另外,一些开发者可以使用第三方平台,如 DooPHP、DooWeb 和 DooSvn 等,以使 Web 和 Native 的代码库更加高效。

定位和修复性能问题

在经过了性能分析及优化之后,可能有些问题无法完全解决。还需使用监测工具进行性能优化,使用 react-devtools 针对 App 进行调试。

React devtools 支持从 Chrome 控制台检查应用程序,并提供了一整套调试工具。此外,也可以使用 react-native-debugger 进行针对应用的性能测试。

性能测试流程大致如下:

  1. 使用数据量较大的列表进行测试;
  2. 监测页面渲染时间,并确定需要优化的目标;
  3. 经过优化后进行二次测试,直到达到最终目标。
const start = new Date();
await fetchData();
const end = new Date();
console.log(`Time to fetch data: ${end.getTime() - start.getTime()}ms`);

使用 console.time 和 console.timeEnd 计时来确定代码执行时间。

性能优化实践

以下给出一些编写 React Native 代码的实践方法,以提高性能。

FlatList 中的 keyExtractor

在 FlatList 中使用 keyExtractor,每个列表的 Item 都会被赋予唯一的 ID,视图的重新排列和插入会快很多。如下例:

render() {
  return (
    <FlatList
      data={this.state.list}
      keyExtractor={(item, index) => item + index}
      renderItem={({ item }) => {
        return <Text>{item}</Text>;
      }}
    />
  );
}

state 不可变性

应该尽量保持 state 的不可变性,即不应该去修改它。在某些情况下,需要在 state 中添加或删除项。最好使用 concat 和 filter 来创建新数组,以确保不破坏原来的 state。如下例:

pressHandler = (item) => {
  this.setState((prevState) => {
    return {
      data: prevState.data.filter((i) => i !== item),
    };
  });
};

Class 转向 PureComponent

在不涉及到 ShouldComponentUpdate 时,使用 PureComponent 代替你的 Component 。

class Component1 extends PureComponent {
  render() { ... }
}

class Component2 extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.title !== this.props.title) {
      console.log('shouldComponentUpdate', nextProps, nextState);
      return true;
    }
    return false;
  }

  render() { ... }
}

其他

  • 避免使用大量的 spread 操作符,降低组件性能;
  • 减少不必要的布局计算;
  • 使用 animation() 函数,而不是 Timing 变量;
  • 使用高阶组件避免组件树状下用 props 传 jsx。

总结

React Native 有着先进的机制和不断优化的代码库,是一款优秀的跨平台开发框架。却也因为底

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65952973eb4cecbf2d961207


纠错反馈