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

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

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

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

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

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

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

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

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

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

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

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

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

RN的加載流程主要為幾個(gè)階段

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

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

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

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

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

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

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

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通用化建設(shè)與性能優(yōu)化 - Web前端 騰訊IVWeb 團(tuán)隊(duì)社區(qū)
不愧是騰訊,主要講了通用化建設(shè)、bundle本地分包、項(xiàng)目線上性能分析幾項(xiàng)
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延遲加載模塊或者文件,直到真的需要它們

看個(gè)小例子就很容易明白了

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';

// 先把這個(gè)組件賦值為null
let VeryExpensive = null;

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

  didPress = () => {
    if (VeryExpensive == null) {
        // 真正需要這個(gè)組件的時(shí)候才加載
      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)化

那么進(jìn)入App后的性能點(diǎn)又在哪里呢?還是回到Bridge

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

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

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

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

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

每個(gè)小點(diǎn)主要會(huì)按照容易實(shí)施執(zhí)行的順序來寫

一、是否重新渲染——shouldComponentUpdate

生命周期請(qǐng)看官方文檔React.Component - React

react應(yīng)用中的state和props的改變都會(huì)引起re-render

考慮下面這種情況

class Home extends Component<Props> {
  constructor(props) {
    super(props);
    this.state = {
      a: '點(diǎn)我看看會(huì)不會(huì)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ù),這會(huì)造成多大的性能浪費(fèi)

加上shouldComponentUpdate鉤子看看如何

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

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

嗯,這下好了點(diǎn),不會(huì)無腦渲染了

那么假如是個(gè)引用對(duì)象呢?

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永遠(yuǎn)指向同一個(gè)引用對(duì)象obj,雖然每次點(diǎn)擊的時(shí)候,obj.num都會(huì)被改變
但是,頁面會(huì)不會(huì)重新渲染呢?
繼續(xù)看圖

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

那么加上shouldComponentUpdate比較一下呢?

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

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

到這里應(yīng)該能很好的解釋了shouldComponentUpdate的特點(diǎn)

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

另外,還有個(gè)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”.

說到底,也只是會(huì)自動(dòng)使用shouldComponentUpdate鉤子的普通Component而已,沒什么特殊的

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

1)InteractionManager

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

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

2)requestAnimationFrame

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

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

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

3) setNativeProps

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

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

因此這個(gè)方式官方都不再推薦,更推薦的做法是合理使用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.

三、動(dòng)畫

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

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

直接setState

LayoutAnimation效果1


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

使用很簡(jiǎn)單,分為兩種情況

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

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

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