使用Pure Component
在React项目中,渲染复杂(也可以是所有组件)的组件最好继承于PureComponent,这样通过shouldComponentUpdate可以避免不必要的render调用和虚拟DOM的比较。
// ES6 写法
import React, { PureComponent } from 'react'
export default class Banner extends PureComponent {
// ...
}
// React.createClass写法
import React from 'react'
import PureRenderMixin from 'react-addons-pure-render-mixin'
const Banner = React.createClass({
mixins: [ PureComponent ],
// ..
})
合理使用箭头函数(尽量避免)
在定义组件时,我们经常会用到箭头函数,比如在ES6 class中为了保持住this的引用,定义ref
import React, { PureComponent } from 'react'
export default class Banner extends PureComponent {
render() {
return (
<FlatList
ref={ v => this._scrollRef = v }
onScroll={evt => this._onScroll(evt)}
data={this.props.listData}
renderItem={(...args) => this._renderItem(...args)}
extraData={{
id: this.props.id,
}}
/>
)
}
}
以上写法有一个问题,这样写每次调用Banner的render方法,ref, onScroll, renderItem, extraData都会重新创建,这样会使FlatList的PureComponent优化失效,从而导致不必要的render调用和虚拟DOM比较。
比较好的写法应当如下
import React, { PureComponent } from 'react'
export default class Banner extends PureComponent {
_setScrollRef = (v) => {
this._scrollRef = v;
}
_extraData = {}
_getExtraData = () => {
if (!this._extraData || this._extraData.id !== this.props.id) {
this._extraData = {
...this._extraData,
id: this.props.id,
}
}
return this._extraData;
}
render() {
return (
<FlatList
ref={ this._setScrollRef }
onScroll={ this._onScroll }
data={this.props.listData}
renderItem={ this._renderItem }
extraData={this._getExtraData()}
/>
)
}
_renderItem = (v) => {
// ...
}
_onScroll = (v) => {
// ...
}
}
如上面这种写法,如果id或listData不变,则FlatList将不会被无效调用render。
当然,并不是说你不能在项目中使用箭头函数,但当你的项目存在性能问题时,你应当考虑移除一些箭头函数。
组件接受最小单位的数据
在我们的业务项目中,通常都会用Redux管理数据,然后通过connect来将store中数据注入组件,也就是所谓的容器组件。
在定义容器组件的时候,一定要注意的是,只关注这个容器组件及其自组件所关心的数据,不要订阅无关数据。
// 方式一
connect((state) => {
return state.homeData;
}, HomeActions)(ViewPager);
// 方式二
connect((state) => {
return state.homeData.viewPagerData;
}, HomeActions)(ViewPager);
/**
* homeData数据结构
* {
* navList: [],
* cid: 0,
* viewPagerData: [],
* adUrl: null,
* }
*/
其实ViewPager所关注的只有viewPgaerData,如果如上面方式一订阅homeData下的所有数据,那当homeData中的其他字段变化,都会出发ViewPager render函数的调用和虚拟DOM比较。而方式二只订阅了ViewPager所关注的数据,这样其他字段的变化就不会出发到ViewPager的无效渲染。
使用Immutable数据
在使用Redux和PureComponent时,最好结合Immutable Data一起使用,这可以使props比较的更高效和准确。
一般比较好的做法是在Redux Store中将数据Immutable化和扁平化,在使用connect包装容器组件时,可以直接让其接收Immutable的数据。
使用FlatList/SectionList
当我们页面上需要展示一个很长的列表并且列表每一项图片还很多时,请使用FlatList或SectionList。
如果使用的还是ScrollView,对于长列表,RN会将所有项一次性全部渲染出来,这会出现两个问题,首先首次渲染用时会很长,即时在视窗范围外的节点也会全部被渲染,其次如果图片多了,会导致系统OOM。
ListView比ScrollView稍好,它带有分页功能,但它是增量渲染,也就是前面渲染的列表项不会在被销毁,会一直留在页面上,当滑动到底部,它和ScrollView展示的节点数就一致了,所以使用ListView并不能避免OOM。
FlatList和SectionList都是基于VirtualizedList使用的,在使用这两个组件时,默认会渲染当前屏幕的数据,然后上面10屏高度的数据,下面10屏高度的数据,这个设置可以通过windowSize来修改,所以在使用这两个列表组件时,就不存在增量的问题,不会出现OOM。
ScrollTabView的使用
ScrollTabView是用来做多Tab页切换的,当经常每个tab下都是一个长列表,如果一个一个切换到最后一个tab而不做任何优化的,而每个Tab下图片又很多的话,很大概率会出现OOM。
可以通过下面方式避免:
export default class ViewPager extends PureComponent {
render() {
return (
<ScrollTabView
ref="tabView"
scrollWithoutAnimation={false}
rerendingSiblingNumber={1}
>
{this.props.naviList.map((nav, i) => {
return (
<TabPage
tabLabel={nav.name}
onTabSwitch={this._onTabSwitch}
key={`tabpage-${nav.id}`}
/>
);
})}
</ScrollTabView>
)
}
}
class TabPage extends PureComponent {
render() {
const tabSelected = this.props.currentTabIndex === this.props.tabIndex;
return (
<View style={{flex: 1, overflow: 'hidden', borderWidth: 0}}>
<Image hidden={!tabSelected} source={{uri: 'https://xxx.png'}} />
</View>
);
}
}
在每一个TabPage组件中,ScrollTabView会自动注入两个props,currentTabIndex表示当前显示的tab页的索引,tabIndex表示tab页自己的索引,当两者不一致,代表这个tab页不在视窗范围内,可以通过Image的hidde把正在显示的图片隐藏。
Android过渡绘制
在Android平台上,设置背景色当遵循设置最小化。因为大范围的设置背景色会导致过度绘制
内存优化
图片内存优化
在原生开发中,如果图片展示过多而得不到释放,会出现OOM的情况。所以在长列表或多Tab页面中,一定要想办法对图片进行回收,在RN里我们可以将不展现在视图范围内的图片用一个空白的View来代替,这样Native底层会对缓存的图片进行gc
View结构过多过于复杂优化
如果一个页面中创建的View过多,View嵌套过深,都会对性能产生不利影响。
对于View创建过多,可以从销毁不在视图范围内的View入手
对View嵌套过深的问题,可以想办法通过样式来精简
页面堆栈优化
在RN中,通过push方式切换页面,路由栈中原有页面并不会被销毁,而是通过translate方式移除到屏幕之外的地方,所以之前显示页面所占内存也并未被销毁,我们不应当无限制的往路由栈中压栈。
举个例子:在商品详情页下面有联想的相关商品,可以通过相关商品往路由栈无限的压商详页,原生处理方式是最多只保留当前商详页和最近两个的商详页,其他予以销毁。
参照原生处理,在RN中,我们也是可以做到这样处理的。可以通过Router中的reset(routes: Array<RouteType>, index)手动重置路由栈。