React-Native大型项目爬坑实录与性能调优

React-Native爬坑实录与性能调优

最近入职mini项目在技术选型的时候掉到了“你们都熟悉React技术栈,那咱们就用React-Native来进行移动端开发吧”的坑里面。在Facebook已经宣布要对RN进行重构的时间节点上选择这么一个技术栈来进行较大型移动端的开发,实在不是一个明智之举。react

虽然如此,在两周的开发中也积累了很多之后能够用到的经验,在这里分享一下。固然性能问题对于不一样类型的应用来讲,其痛点也不尽相同,全部优化方法的使用也是和本身当前的项目内容密切相关的。android

深刻理解React生命周期

不管RN对于组件进行了多少封装,其runtime仍然仍是离不开React自己的生命周期的,在出现了和你预期渲染结果不相同的问题的时候,大多数都是由于React内部渲染的过程与你的代码产生了冲突。web

TextInput绑定(移动端虚拟键盘)

一个常见的UGC类移动端APP,都会有让用户输入内容的地方,好比发布、评论等。因为移动端键盘会在屏幕底部弹出,本来吸附在屏幕底部的TextInput组件须要向上弹起,而且吸附在键盘的上方,这样才能获得最好的输入体验。经过position: "absolute"定位以后的输入框,随着键盘弹起事件的发生,须要将其高度进行调整:redux

componentWillMount() {
    this.keyboardWillShowListener = Keyboard.addListener('keyboardWillShow', this._keyboardWillShow.bind(this));
    this.keyboardWillHideListener = Keyboard.addListener('keyboardWillHide', this._keyboardWillHide.bind(this));
}
_keyboardWillShow(event) {
    this.setState({
        keyboardHeight: event.endCoordinates.height,
        keyboardShow: true
    });
}

在键盘进行输入的时候,为了保证每次提交评论均可以清空掉评论输入框之中的内容,须要为TextInput组件绑定value到state上面来进行输入框值的实时获取,而且在提交的异步操做完成以后,进行该值的清空。数组

可是这样作会出现一个问题,那就是中文输入法因为每次组件的从新渲染,不可以展现中文输入的预选,致使不可以经过键盘输入中文。这样的问题能够经过两个方法来解决:性能优化

  1. 将TextInput所在的组件经过PureComponent进行扩展,这样在进行浅比较的时候,不会触发shouldComponentUpdate致使的从新渲染;
  2. 手动设置showComponentUpdate的返回值,强制让中文的预输入不从新渲染组件。

第二种方法是一种更加灵活的方法,也是对于React生命周期阶段的更好的利用。可是在使用第二种方法的时候,要注意组件的其余props和states,当且仅当输入框内容发生变化的时候,不触发从新渲染。babel

shouldComponentUpdate(nextProps, nextState){
    const flag = Object.keys(nextState).map((k) =>(nextState[k] !== this.state[k])).filter(Boolean).length == 1
    if(flag && nextState.commentText !== this.state.commentText){
        return false;
    }
    return true;
}

什么时候使用Component,什么时候使用PureComponent

PureComponent会根据一层props和states的浅比较来判断是否re-render一个组件,这一层浅比较会对比简单值的变化以及复杂值的引用变化,因此,即便调用了setState方法,若是采用push这种数据方法来为数组增长一个元素,也不会对于数组的渲染内容进行re-render。网络

深层的引用元素在PureComponent中是很危险的,一些不注意的操做都会致使渲染结果的不肯定性。在RN中也是一致的。APP中常见的列表元素的渲染,通常都是使用数组方式传入FlatList或者SectionList中的。而这些数据大部分都是从服务端进行获取的。每次都是一个全新的数组,即便数组的数据没有发生变化,也会形成列表的从新渲染。架构

在这种状况下,既然很难避免列表的re-render,那么列表项的re-render就能够很好地经过PureComponent来进行性能优化。数组在传入到FlatList的data属性当中,被解析为较为扁平的数据,若是咱们将这个列表项数据彻底解析为扁平化的数据,就能够很好地利用PureComponent的浅比较,来尽量减小列表项的re-render数量,增长必定的刷新或者加载性能。app

因此,PureComponent并必定是最好的选择,他有可能会致使组件不可以完成咱们须要的渲染操做,可是在扁平状态的叶子组件上,进行没有太多逻辑改变的展现,能够再很大程度上增长整个项目的组件复用性。

路由切换优化

在APP的第一次迭代完成以后,咱们发现整个项目的稳定性不好,在须要渲染长列表的发现页面中,几回比较快速的路由切换操做就会致使整个软件闪退,这样的体验是不可以接受的。

开始分析的闪退缘由是下面几个:

  1. 图片太大,致使内存溢出;
  2. DOM节点数过多(<View>的层层嵌套),致使每一次路由切换时候的组件卸载、挂载阻塞RN或者Native;
  3. 地图组件的加载以及地图标点功能阻塞渲染;
  4. 路由切换时过多的网络请求阻塞Native。

虚拟DOM的问题在移动设备上体现了出来。上一个路由的DOM仅仅会被Unmount,为了路由返回时候的复用,可是一部分移动设备仅仅有2G或者更小的RAM,这样过多的DOM节点会致使内存泄露。而且一次性阻塞地渲染几百上千个DOM节点也会让Native不堪重负。

通过几回测试,咱们压缩了发现页的图片大小,可是闪退并无很是明显的优化。而且经过Mock数据,将网络请求压缩为一个,也没有很好地解决这个问题。最后咱们将问题定位到了上述2和3两点。

既然是阻塞问题,那么咱们将整个页面进行切分,地图组件自己带有不少的DOM节点,而且须要很长时间进行网络加载,而且相对于列表,地图组件并非很是重要。因此地图组件能够被延迟加载。

列表组件的渲染也应该是在网络请求和RN路由动做完成以后再进行的,因此咱们将数据的dispatch和RN路由动做做为第一优先级的事情去作,而将列表组件做为第二优先级,地图做为末尾,造成了一个页面的加载链。

RN的InteractionManager能够帮咱们细化整个阶段。将页面的渲染置于页面切换动做完成以后进行。而地图则采用定时器进行延迟加载。

await Promise.all(
    [
        new Promise((resolve, reject) => {
            setTimeout(() => {
                this.setState({
                    mapVisible: true
                }, resolve);
            }, 1000)}), 
        dispatch(initDiscoveryItem({
            feedId: this.feedId,
        }))
    ]
).then(() => {
    this._getLocations(this.props.data);
});

InteractionManager.runAfterInteractions(() => {
    this.setState({
        startRender: true
    });
});

地图的加载被设置了一个1秒的延迟,而且包裹一个Promise,数据的异步加载也是一个Promise,地图坐标定位将会被延迟到地图加载和数据加载以后进行,而当整个页面切换动做完成以后,开始进行整个页面的渲染。

最后的效果是APP的发现页部分进行阶段性渲染:

  1. 路由切换动做;
  2. 数据加载;
  3. 页面组件渲染;
  4. 地图组件在路由切换以后一秒进行渲染;
  5. 地图多点定位在全部任务都完成以后进行。

在正常操做的状况下,基本上不会出现页面切换闪退的状况了。

HOC的使用

HOC是React一个老生常谈的话题了。为了更好地进行代码的复用,整个项目分为了三个主要的层次进行架构。

  1. 基本组件:这些组件实现了扁平数据的基本展示功能;
  2. 基本的组件的Wrapper,HOC:这些组件并不包含太多的渲染任务,他将组件进行保证,Mixin一些通用功能;
  3. 路由组件:这些组件是一个路由页面的核心,里面会渲染多个基本组件以及包裹了基本组件的HOC。

HOC其实并不复杂,它接收一个或者多个组件做为参数,返回一个包装后的组件。

平台适配

对于RN来讲,为了进行iOS和android的适配,不少地方都须要根据这两个平台的特性进行协调,那么这些协调的代码若是可以复用,采用HOC能够减小不少的代码量,而且加强复用性。

一个UGC社区会有不少须要键盘输入的地方,那么一个通用的键盘谈起的跨平台方案就是必须的,因为android自己比iOS设备会多出一个虚拟的返回键,对于返回键就须要进行特殊的处理。

可是其余部分的键盘逻辑实际上是能够复用的,若是将平台处理放在组件内部进行也是能够的,可是这样会致使代码的可读性很低,咱们将键盘返回键的处理放在一个高阶组件内部。

(Component) => {
    class Wrapper extends PureComponent {
        constructor(props) {
            super(props);
        }
        componentWillMount() {
            this,keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
            this,keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
        }
        // android only 
        handleOnRequestClose() {
            ...
            if (!this.isShowkeyboard) {
                this.close();
                this.setState({
                    isKeyboardClose: false
                });
            }
        }
        render() {
            return <Component {...this.props}/>
        }
    }
}

这个高阶组件省略了一部分代码,使用这个组件将咱们须要进行适配的TextInput注入进去,就能够获得一个能够对android键盘事件进行特殊处理的组件。这样也作到了SOLID原则中的开放封闭原则,咱们的组件对于扩展开放,每次进行适配代码修改的时候,不须要修改每个组件,只须要修改一个HOC就能够了。

渲染拦截

loading状态是大部分SPA都须要的一个状态,来为用户展现一个加载中的动画,让用户进行有明确目的的等待。不少页面都要这样一个状态来完成整个业务流程。这个状态也能够经过高阶组件进行封装。

(Component) => {
    class Wrapper extends PureComponent {
        constructor(props) {
            super(props);
        }
        render() {
            const {
                isLoading
            } = this.props;
            return (
                isLoading 
                ? <Loading/>
                : <Component {...this.props}/>
            )
        }
    }
}

每一个须要loading状态的组件经过这个HOC均可以直接传入须要的参数和组件,封装一个具备加载状态的组件,咱们拦截掉了本来的组件渲染,为其增长了一个额外的外部渲染逻辑。

若是你的组件须要一个空态,或者是须要一个统一的错误处理,均可以使用这种方式来进行实现。

props的CRUD

react-redux就是一个很是好的例子,react-redux中的connect方法对于本来的组件进行了代理,将store中数据经过reducer进行切分,而后Mixin到组件的props当中。

async/await

做为ES7中的异步处理方法,经过babel咱们能够很容易在咱们的项目中使用它。目前整个项目中的异步数据操做都采用async+Promise的方法来进行实现。

当多个异步事件须要异步或者同步进行的时候,咱们经过结合两种异步方法,就能够获得很清晰的代码逻辑。

const initData = async () -> {
    try(
        await AsyncStorage.get('userId', (err, result) => {
            if (err) {
                errHandler(err);
            } else {
                this,userId - result;
            }
        });
        await Promise.all(
            [dispatch(getInitUserData({
                userId,
            }), 
            dispatch(getInitPassageData({userId}]
            ))).then(data => {
                dataHandler(data);
        });
    } catch(e) {
        this._errorHandler(e);
    }
}

后记

虽然RN存在很多坑须要去踩,而且其进行和Native相关的组件配置的时候存在不少配置问题,可是做为一种移动端同构方式,也是很是具备前瞻性的,和webview相比,RN不须要使用JSBridge的方式来进行Native和web的通讯,这得益于React自己社区的强大,但愿facebook重构后的RN可以具备更好的性能和更轻松的开发体验。

相关文章
相关标签/搜索