React Native 是一款跨平台的移动应用开发框架,能够让开发者用 JavaScript 开发出类似于原生应用的用户体验。然而,由于框架底层的设计和实现机制,React Native 开发中有时会遇到性能问题。本文将在深入分析 React Native 开发中出现的性能问题的原因的基础上,给出性能优化的方法和实践经验。
性能问题的表现和原因
在开发 React Native 应用时,常见的性能问题主要有以下几种表现形式:
- 首次渲染过慢:组件初次渲染加载的时间过长;
- 卡顿:操作时页面反应迟钝,出现卡顿现象;
- 内存占用过高:程序运行一段时间后,内存占用过高,导致程序崩溃或者运行缓慢;
- 视图绘制闪烁:页面中视图重复渲染,导致页面绘制闪烁。
造成上述问题的原因大致如下:
- JS 线程阻塞:React Native 应用运行在单线程模式下,当 JS 线程执行任务耗时较长时,会阻塞渲染线程,导致页面卡顿;
- 内存泄漏:内存一直增长,最终耗尽导致崩溃;
- 视图重复渲染:部分组件没有开启 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 进行针对应用的性能测试。
性能测试流程大致如下:
- 使用数据量较大的列表进行测试;
- 监测页面渲染时间,并确定需要优化的目标;
- 经过优化后进行二次测试,直到达到最终目标。
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