[轉(zhuǎn)]React Native 性能優(yōu)化總結(jié)

轉(zhuǎn)載https://github.com/amandakelake/blog/issues/49

最近在進行RN項目重構(gòu),通過查閱各種資料,從RN底層出發(fā),思考總結(jié)了一些從react到react-native的性能優(yōu)化相關(guān)問題

Performance · React Native
請先認真查看官方文檔(英文文檔)這一章節(jié)
前方高能請注意:Unbundling + inline requires這一節(jié),中文文檔木有!?。?/p>

先看看可能會導致產(chǎn)生性能問題的常見原因

這里先給出我自己的結(jié)論,然后會從底層原理開始理解為何要這樣做,最后是每項方法的具體展開(未完待續(xù))

這部分都不是死知識,可能哪天我又會有更廣闊的思路與解決辦法,或許會推翻現(xiàn)在的結(jié)論,所以本文會持續(xù)保持更新。。。

RN性能優(yōu)化概述

談性能之前,我們先了解一下RN的工作原理

通過RN我們可以用JS實現(xiàn)跨平臺App,也就是FB說的write once, run everywhere

RN為我們提供了JS的運行環(huán)境,所以前端開發(fā)者們只需要關(guān)心如何編寫JS代碼,畫UI只需要畫到virtual DOM 中,不需要特別關(guān)心具體的平臺

至于如何把JS代碼轉(zhuǎn)成native代碼的臟活累活,RN底層全干了

RN的本質(zhì)是把中間的這個橋Bridge給搭好,讓JS和native可以互相調(diào)用

RN的加載流程主要為幾個階段

  • 初始化RN環(huán)境
    • 創(chuàng)建Bridge
    • Bridge中的JS環(huán)境
    • RN模塊、UI組件
  • 下載JS Bundle
  • 運行JS Bundle
  • 渲染頁面

Dive into React Native performance | Engineering Blog | Facebook Code | Facebook

通過對FaceBook的ios版進行性能測試,得到上面的耗時圖
可以看到,綠色的JS Init + Require占據(jù)了一大半的時間,這部分主要的操作是初始化JS環(huán)境:下載JS Bundle、運行JS Bundle

JS Bundle 是由 RN 開發(fā)工具打包出來的 JS 文件,其中不僅僅包含了RN 頁面組件的 JS 代碼,還有 react、react-native 的JS代碼,還有我們經(jīng)常會用上的redux、react-navigation等的代碼,RN 非常簡單的 demo 頁面minify 之后的 JS Bundle 文件有接近 700KB,所以 JS Bundle文件大小是性能優(yōu)化的瓶頸

假設我們有一個大型App,它囊括了非常多的頁面,但是在常規(guī)使用中,很多頁面甚至都不會被打開,還有一些復雜的配置文件以及很少使用的功能,這些相關(guān)的代碼,在App啟動的時候都是不需要的,那么,我們就可以考慮通過Unbundling拆包來優(yōu)化性能

關(guān)于如何減少Bundle包的大小,目前主流的方法是拆分Bundle包,把框架代碼和業(yè)務代碼單獨出來,框架代碼非常大,因此要分離出來單獨前置加載,而業(yè)務代碼則變成很小的JS代碼單獨發(fā)布,下面提供一些前人的經(jīng)驗鏈接

但在拆包之前,F(xiàn)B官方還提了幾條在此之前更應該做好的優(yōu)化點

Doing less

  • Cleanup Require/Babel helpers
  • Avoid copying and decoding strings when loading the bundle
  • Stripping DEV-only modules

Scheduling

  • Lazy requires
  • Relay incremental cache read
  • De-batching bridge calls, batch Relay calls
  • Early UI flushing
  • Lazy native modules loading
  • Lazy touch bindings on text components

React-Native通用化建設與性能優(yōu)化 - Web前端 騰訊IVWeb 團隊社區(qū)
不愧是騰訊,主要講了通用化建設、bundle本地分包、項目線上性能分析幾項
RN分包之Bundle改造
RN 打包那些事兒 | YMFE
React Native拆包及熱更新方案 · Solartisan

說到unbundling,官方文檔還把 inline requires 一并合起來分析了

Inline requires delay the requiring of a module or file until that file is actually needed.
inline requires延遲加載模塊或者文件,直到真的需要它們

看個小例子就很容易明白了

import React, { Component } from 'react';
import { Text } from 'react-native';
// ... import some very expensive modules

// You may want to log at the file level to verify when this is happening
console.log('VeryExpensive component loaded');

export default class VeryExpensive extends Component {
  // lots and lots of code
  render() {
    return <Text>Very Expensive Component</Text>;
  }
}
import React, { Component } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';

// 先把這個組件賦值為null
let VeryExpensive = null;

export default class Optimized extends Component {
  state = { needsExpensive: false };

  didPress = () => {
    if (VeryExpensive == null) {
        // 真正需要這個組件的時候才加載
      VeryExpensive = require('./VeryExpensive').default;
    }

    this.setState(() => ({
      needsExpensive: true,
    }));
  };

  render() {
    return (
      <View style={{ marginTop: 20 }}>
        <TouchableOpacity onPress={this.didPress}>
          <Text>Load</Text>
        </TouchableOpacity>
          // 根據(jù)需要判斷是否渲染該組件
        {this.state.needsExpensive ? <VeryExpensive /> : null}
      </View>
    );
  }
}

Even without unbundling inline requires can lead to startup time improvements, because the code within VeryExpensive.js will only execute once it is required for the first time

上面的內(nèi)容主要是關(guān)于首屏渲染速度的性能優(yōu)化

那么進入App后的性能點又在哪里呢?還是回到Bridge

首先,在蘋果和谷歌兩位大佬的光環(huán)下,native代碼在設備上的運行速度毋容置疑,而JS作為腳本語言,本來就是以快著稱,也就是說兩邊的獨立運行都很快,如此看來,性能瓶頸只會出現(xiàn)在兩端的通信上,但兩邊其實不是直接通信的,而是通過Bridge做中間人,查找、調(diào)用模塊、接口等操作邏輯,會產(chǎn)生到能讓UI層明顯可感知的卡頓,那么性能控制就變成了如何盡量減少Bridge所需要的邏輯。

  • UI事件響應
    這塊內(nèi)容都發(fā)生在Native端,以事件形式傳遞到JS端,只是一個觸發(fā)器,不會有過度性能問題
  • UI更新
    JS是決定顯示什么界面,如何樣式化頁面的,一般都是由JS端發(fā)起UI更新,同時向native端同步大量的數(shù)據(jù)和UI結(jié)構(gòu),這類更新會經(jīng)常出現(xiàn)性能問題,特別是界面復雜、數(shù)據(jù)變動量大、動畫復雜、變動頻率高的情況
  • UI事件響應+UI更新
    如果UI更新改動不大,那么問題不大
    如果UI事件觸發(fā)了UI更新,同時邏輯復雜、耗時比較長,JS端和Native端的數(shù)據(jù)同步可能會出現(xiàn)時間差,由此會引發(fā)性能問題

總結(jié)起來,核心的RN性能優(yōu)化點就比較清晰明朗了

  • 首屏渲染優(yōu)化:處理JS Bundle包大小、文件壓縮、緩存
  • UI更新優(yōu)化
    • 減少更新或者合并多個更新
    • 提高組件響應速度:
      • setNativeProps直接在底層更新Native組件屬性(其實沒有解決JS端與Native端的數(shù)據(jù)同步問題)
      • 立即執(zhí)行更新回調(diào)
    • 動畫優(yōu)化
      • 通過使用Annimated類庫,一次性把更新發(fā)送到Native端,由Native端自己負責更新
      • 把一些耗時操作放到動畫與UI更新之后執(zhí)行
  • 其他優(yōu)化(代碼層面)

[圖片上傳失敗...(image-f00a3f-1552026313535)]

每個小點主要會按照容易實施執(zhí)行的順序來寫

一、是否重新渲染——shouldComponentUpdate

生命周期請看官方文檔React.Component - React

react應用中的state和props的改變都會引起re-render

考慮下面這種情況

class Home extends Component<Props> {
  constructor(props) {
    super(props);
    this.state = {
      a: '點我看看會不會re-render',
    }
  }

  render() {
    console.log('重新渲染   re-render------------------');
    return (
      <View style={styles.container}>
        <TouchableOpacity style={styles.addBtn} onPress={() => this.setState({ a: this.state.a })}>
          <Text>{this.state.a}</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

核心代碼是this.setState({ a: this.state.a })

明明沒有改變a,只是setState了一下而已,就直接觸發(fā)了重新渲染,試想一下,如果頁面有大型數(shù)據(jù),這會造成多大的性能浪費

加上shouldComponentUpdate鉤子看看如何

  shouldComponentUpdate(nextProps, nextState) {
    return nextState.a !== this.state.a
  }

[圖片上傳失敗...(image-53602d-1552026313535)]

嗯,這下好了點,不會無腦渲染了

那么假如是個引用對象呢?

const obj = { num: 1 };
class Home extends Component<Props> {
  constructor(props) {
    super(props);
    this.state = {
      b: null
    }
  }

  componentWillMount() {
    this.setState({
      b: obj
    })
  }

  render() {
    console.log('重新渲染   re-render------------------');
    return (
      <View style={styles.container}>
        <TouchableOpacity style={styles.addBtn} onPress={() => {
          obj.num++;
          this.setState({
            b: obj
          })
        }}>
          <Text>{this.state.b.num}</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

給b永遠指向同一個引用對象obj,雖然每次點擊的時候,obj.num都會被改變
但是,頁面會不會重新渲染呢?
繼續(xù)看圖

很好,對象的內(nèi)容變了,頁面也重新渲染

那么加上shouldComponentUpdate比較一下呢?

shouldComponentUpdate(nextProps, nextState) {
    return nextState.b !== this.state.b
  }

頁面毫無變化
原因:b每次都指向了同一個引用對象obj,引用地址沒變,shouldComponentUpdate只會做淺比較,自然會返回false,頁面不會重新渲染

到這里應該能很好的解釋了shouldComponentUpdate的特點

那么如何處理引用對象的情況呢?目前最推崇的做法是使用不可變對象immutablejs,facebook自家出的
GitHub - facebook/immutable-js
好了,研究去吧

另外,還有個pureComponent,看下官方介紹就好了
React Top-Level API - React

React.PureComponent
React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.

If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.

Note

React.PureComponent’s shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.

Furthermore, React.PureComponent’s shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also “pure”.

說到底,也只是會自動使用shouldComponentUpdate鉤子的普通Component而已,沒什么特殊的

二、組件響應速度(InteractionManager、requestAnimationFrame、setNativeProps)

1)InteractionManager

InteractionManagerrequestAnimationFrame(fn)的作用類似,都是為了避免動畫卡頓,具體的原因是邊渲染邊執(zhí)行動畫,或者有大量的code計算阻塞頁面進程。
InteractionManager.runAfterInteractions是在動畫或者操作結(jié)束后執(zhí)行

InteractionManager.runAfterInteractions(() => {
  // ...long-running synchronous task...
});

2)requestAnimationFrame

window.requestAnimationFrame - Web API 接口 | MDN
使用requestAnimationFrame(fn)在下一幀就立即執(zhí)行回調(diào),這樣就可以異步來提高組件的響應速度;

OnPress() {
  this.requestAnimationFrame(() => {
    // ...setState操作
  });
}

還有setImmediate/setTimeout(): 這個是比較原始的奔方法,很有可能影響動畫的流暢度

3) setNativeProps

Direct Manipulation · React Native
通過Direct Manipulation的方式直接在底層更新了Native組件的屬性,從而避免渲染組件結(jié)構(gòu)和同步太多視圖變化所帶來的大量開銷。

這樣的確會帶來一定的性能提升,同時也會使代碼邏輯難以理清,而且并沒有解決從JS側(cè)到Native側(cè)的數(shù)據(jù)同步開銷問題。

因此這個方式官方都不再推薦,更推薦的做法是合理使用setState()和shouldComponentUpdate()方法解決這類問題。

Use setNativeProps when frequent re-rendering creates a performance bottleneck
Direct manipulation will not be a tool that you reach for frequently; you will typically only be using it for creating continuous animations to avoid the overhead of rendering the component hierarchy and reconciling many views. setNativeProps is imperative and stores state in the native layer (DOM, UIView, etc.) and not within your React components, which makes your code more difficult to reason about. Before you use it, try to solve your problem with setState and shouldComponentUpdate.

三、動畫

Animated的前提是盡量減少不必要的動畫,具體的使用方式請看官方文檔Animated · React Native

如果覺得Animated的計算很麻煩,比如一些折疊、增加減少view、改變大小等簡單的操作,可以使用LayoutAnimation來流暢的完成一次性動畫
看下直接setState和使用LayoutAnimation后的效果對比

直接setState

LayoutAnimation效果1


LayoutAnimation效果2
2018-06-11 10 30 38

使用很簡單,分為兩種情況

  • 使用默認的效果
    componentWillUpdate鉤子里面,讓整個組件所有動畫都應該該效果,或者在單獨需要動畫的setState方法前面使用LayoutAnimation.spring();
componentWillUpdate() {
    // spring, easeInEaseOut, linear
    LayoutAnimation.linear();
  }
  • 使用自定義的效果
componentWillUpdate() {
    LayoutAnimation.configureNext(config)
  }
const config = {
  duration: 500, // 動畫時間
  create: {
  // spring,linear,easeInEaseOut,easeIn,easeOut,keyboard
    type: LayoutAnimation.Types.linear,
  // opacity,scaleXY 透明度,位移
    property: LayoutAnimation.Properties.opacity,
  },
  update: {
  // 更新時顯示的動畫
    type: LayoutAnimation.Types.easeInEaseOut,
  }
};
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容