iOS

react native 性能优化 总结

2020-07-10  本文已影响0人  请叫我啊亮

rn性能优化 结合网上资料总结如下

1、首屏渲染问题。采用JS Bundle拆包解决。就是主体框架react单独打成一个基础包,一旦进入app就马上加载,而相关业务模块单独拆分成多个包,进入相应模块才动态加载。这样可以大大加快APP的启动速度,各个业务也能独立开发,各自维护、下载、更新

2、图片问题。rn开发时本地图标为了统一往往放在js端,极端时(如一个页面加载几十上百张图片)可能会有性能问题。这是因为如果资源从 javascript 包中加载, RN 需要先从包中拿到资源,然后通过bridge把资源传送到 原生UI 层去渲染。而如果资源已经存在在原生端,那么 React 可以直接告知 UI 层去渲染具体的图片,无需通过这个bridge引入或者转入图片资源。 当然不会有这类问题,但是要js端图片要注意压缩,使其不太大,图片越大,性能问题越容易凸显。webP,jpg优先

3、缓存。各种需要的,有必要的缓存,如一个生日日期选择picker组件,数据源大概有100(年)x12(月)x30(天)这么多条数据,如果每次弹出picker都需要计算这些数据,还是会稍微有点延迟,这里可以缓存下来,甚至本地数据存储起来,以后拿出来直接使用

4、延迟加载。页面打开,优先执行那些跟页面展示有关的代码,其他的如埋点,上传状态,gif动画都可以稍后执行。对那些触摸响应事件后才需要展示的组件,或者根据接口返回才能决定是否展示的组件,一开始甚至都可以不用import,直到确定要展示时才局部import导入组件展示。对长列表页面,图片较多时,在页面范围之外的图片可以先不展示,直到滚动后发现图片在屏幕上面显示了再展示

5、动画。普通动画如移动,缩放等直接使用LayoutAnimation,性能更好。复杂点的动画才使用Animated。对帧动画这种需要快速更新state触发动画的场景,可以使用setNativeProps直接修改原生属性(某些场合如背景动画,gif图片可能不是很好的选择,因为gif可能会很大,导致初次解压时出现明显卡顿现象,而且安卓上gif图片首轮显示效果不佳)。
Animated: useNativeDriver为true,则会一次性将动画信息发送给原生端让原生去驱动动画,性能更佳。 否则js端会不断注册定时器事件,让原生端不断回调js方法更改组件的setNativeProps值产生动画,因为动画配置信息在每一帧都在原生和js端通信性能有所损耗,
问题: 为什么不总是使用useNativeDriver? 是因为有些动画原生不支持么?

6、响应速度。由于js是单线程,当在执行一些计算量很大的任务时可能会造成堵塞卡顿现象。此时可以将任务稍微延后执行,避免大量任务在同一个js 事件循环中导致其他任务无法执行。相应的方法有InteractionManager,requestAnimationFrame,setTimeOut(0)等,原理都大同小异

7、刷新问题。每次setState导致的render都会进行一次内存中diff计算,尽管diff效率很高(O(n)),但是还是应该避免不必要的diff。 Pure组件、自定义shouldComponentUpdate实现避免不必要的刷新

8、预加载。对一些重要的,很可能会用到的内容预先加载,例如图片浏览器,当浏览某一张图片时可以预加载前后两张图片,优化用户体验。

9、FlatList的优化。
页面中的重头戏FlatList,尽管经过了大量优化,在数据较多时使用还是需要注意的。
FlatList的频繁刷新问题很常见,如下面

class FlatListTest extends React.Component {

    state = {
        index: 1,
        data: []
    }

    componentDidMount() {
        let data = [];
        for (let index = 0; index < 100; index++) {
            data.push(index);
        }
        this.setState({ data })
    }

    renderItem = (item) => {
        console.log('表格刷新了');
        return (
            <View style={{ height: 50 }}>
                <Text>
                    {item.item}
                </Text>
            </View>
        )
    }
    render() {
        console.log('页面刷新了');
        return (
            <View>
                <FlatList
                    style={{ width: SCREEN_W, height: 444 }}
                    data={this.state.data}
                    keyExtractor={(_, index) => index + ''}
                    renderItem={this.renderItem}
                    ListFooterComponent={<View style={{ width: 100, height: 20, backgroundColor: 'red' }} />}
                />

                <TouchableWithoutFeedback onPress={() => this.setState({ index: this.state.index + 1 })}>
                    <View style={{ width: 100, height: 100, backgroundColor: 'red' }}></View>
                </TouchableWithoutFeedback>

            </View>
        )
    }
}

这样子写FlatList看起来没什么问题,但是性能上完全具有优化空间
点击下方红色按钮让index累加,页面会刷新,但是也会导致FlatList刷新,renderItem被调用98次,就是说页面刷新->表格刷新->所有的表格cell也会刷新。很显然当有大量cell时容易造成性能问题。

FlatList是一个PureComponment,只会对传入的属性进行浅比较(对象地址比较),发现不一样就会刷新。

例子中,FlatList的style,keyExtractor,ListFooterComponent这三个地方传入的对象在页面刷新时会重新生成,导致传入FlatList的属性地址发生变化,FlatList刷新。可以采用下面的方式修复。

    renderFooter = () => {
        return <View style={{ width: 100, height: 20, backgroundColor: 'red' }} />
    }

    keyExtractor = (_, index) => {
        return index + ''
    }

  getItemLayout = (_, index) => {
        return { length: 50, offset: 50 * index, index }
    }

    render() {
        console.log('页面刷新了');
        return (
            <View>
                <FlatList
                    style={styles.flatStyle}
                    data={this.state.data}
                    keyExtractor={this.keyExtractor}
                    renderItem={this.renderItem}
                    ListFooterComponent={this.renderFooter}
                    getItemLayout={this.getItemLayout}
                />

                <TouchableWithoutFeedback onPress={() => this.setState({ index: this.state.index + 1 })}>
                    <View style={{ width: 100, height: 100, backgroundColor: 'red' }}></View>
                </TouchableWithoutFeedback>

            </View>
        )
    }

const styles = StyleSheet.create({
    flatStyle: { width: SCREEN_W, height: 444 }
});

原则就是确保页面刷新后,传入FlatList的所有对象地址不发生变化,这样就不会导致不必要的刷新。

getItemLayout的设置也比较重要,设置后,则列表滚动时,新出现cell时就不用动态去测量cell高度,可以直接从这里拿到,优化性能

把上面的实现改成Hook,会发现页面刷新又会导致表格刷新,因为Hook组件每次刷新时内部的函数都会被重新定义,也就是函数地址发生了变化,从而导致FlatList的刷新。这里需要使用useCallback将所有函数都缓存好,避免函数组件刷新导致函数从新被定义,如下这样,注意依赖

const renderItem = useCallback((item) => {
        console.log('表格刷新了');
        return (
            <View style={{ height: 50 }}>
                <Text>
                    {item.item}
                </Text>
            </View>
        )
    }, [])

本人项目中有个类似微信朋友圈的列表,当数据很多时,在debug环境下点击图片浏览时稍微会有卡顿现象。纠其原因就是因为点击图片浏览触发页面刷新,所有cell跟着刷新完成后才会显示大图导致卡顿,用如上优化后就ok了。

其他:

FlatList显示规则是,在ScrollView上面添加View,只渲染当前展示和即将展示的 View,距离远的 View 用空白 View 展示,从而减少长列表的内存占用。

FlatList的item无法复用,目前了解到的是跟js单线程有关,具体不太明白

重要属性:

getItemLayout,如果不使用,那么所有的 Cell 的高度,都要调用 View 的 onLayout 动态计算高度,这个运算是需要消耗时间的;
为什么需要动态计算每一个View高度? 想一想如果不测量,那么原生端View的Frame如何设置就可以理解了。

windowSize: 表征缓存屏幕外的item多少,单位是一个屏幕显示的item数量。默认为21。例如一个屏幕能显示8个item,那么默认情况下,屏幕上下各缓存10*8个item, 减少该数字能减小内存消耗并提高性能,但是快速滚动列表时,遇到未渲染的空白view几率增大。这里要注意,因为只有当列表停止滚动时才会更新渲染区域,所以只要item足够多,一直滚动不要停止就一定能看到空白view。

maxToRenderPerBatch: 每批次渲染的item个数,默认为10. 例如一个屏幕能显示8个item, 列表停止时默认情况下需要缓存屏幕上下各80个item, 那么需要16个批次才能完成,如果列表停留时间不够用户马上又继续滚动,因为此时缓存的item数量还不够,可能出现滚不动的现象。 如果该值变大则会使所需批次减少,缓存足够item所需时间减小,用户体验更好。 但是如此js一个事件循环任务过多可能导致其他的如列表响应问题。 有时候设置该值是必要的,比如一个长列表,每屏幕能显示下20个item,那么默认情况maxToRenderPerBatch为10就显得太小,滑动时很容易出现滑不动现象,可以适当放大该值。

removeClippedSubviews: 剪切子视图,移除屏幕外较远位置的所有item,优化内存。iOS上面有bug,安卓默认开启。 主要是在ListView时期长列表优化内存使用。

10、hook自定义组件
例如我项目中自定义了个button组件

export const Button = memo((props) => {
    let children = props.children;
    let { disabled, loading, style, onPress } = props;
    if (typeof children == 'string') {
        children = <Text style={{ color: 'white', fontSize: 18 }}>{children}</Text>
    }
    let defaultStyle = {
        height: 45, marginLeft: 15, marginRight: 15, alignItems: 'center', justifyContent: 'center',
        backgroundColor: ColorConf.main(), borderRadius: 5, opacity: loading || disabled ? 0.5 : 1, flexDirection: 'row'
    }
    if (style) {
        defaultStyle = { ...defaultStyle, ...style }
    }
    return (
        <TouchableWithoutFeedback onPress={() => !disabled && onPress && onPress()}>
            <View style={defaultStyle}>
                {loading ? <ActivityIndicator animating={true} color='white' style={{ marginRight: 8 }} /> : null}
                {children}
            </View>
        </TouchableWithoutFeedback>
    )
})

用memo包裹起来跟class时代的pure组件差不多,每次会对传入的props进行浅比较,若不一致才会更新组件

  <Button
      disabled={!(name && password)}
      loading={logining}
      style={{ marginTop: 50 }}
      onPress={_loginInWithPassword} >
        Login In
 </Button>

如果像这样使用,那么每当父组件刷新时,由于传入Button的style是一个临时对象,Button会随着父组件一同刷新,显然是不合适的
同上面,应该如下使用

const _loginInWithPassword = useCallback(() => console.log('点击登陆') },[])

 <Button
      disabled={!(name && password)}
      loading={logining}
      style={styles. buttonStyle}
      onPress={_loginInWithPassword} >
        Login In
 </Button>

const styles = StyleSheet.create({
    buttonStyle: { marginTop: 50 }
});

使用useCallback,useMemo等缓存函数,组件等的时候要注意设置好依赖,否则可能出现值捕获等隐性问题

11、使用Fragment
Fragment和View都可以包裹子元素,但是前者不对应具体的视图,仅仅是代表可以包装而已,跟空的标识符一样

 <React.Fragment>
      <ChildA />
      <ChildB />
    </React.Fragment>

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

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

如上,前面两个完全一样,原生端只存在ChildA和ChildB两个组件。最后那个不一致,对应原生端为View父视图包含ChildA和ChildB两个个组件
视图层级关系减少有利于视图渲染

上一篇下一篇

猜你喜欢

热点阅读