美文网首页iOS
react native 性能优化 总结

react native 性能优化 总结

作者: 请叫我啊亮 | 来源:发表于2020-07-10 15:25 被阅读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两个个组件
    视图层级关系减少有利于视图渲染

    相关文章

      网友评论

        本文标题:react native 性能优化 总结

        本文链接:https://www.haomeiwen.com/subject/wurmcktx.html