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可以具備更好的性能和更輕鬆的開發體驗。

相關文章
相關標籤/搜索