[转]React Native 性能优化总结

转载https://github.com/amandakelake/blog/issues/49

最近在进行RN项目重构,通过查阅各种资料,从RN底层出发,思考总结了一些从react到react-native的性能优化相关问题

Performance · React Native
请先认真查看官方文档(英文文档)这一章节
前方高能请注意:Unbundling + inline requires这一节,中文文档木有!!!

先看看可能会导致产生性能问题的常见原因

这里先给出我自己的结论,然后会从底层原理开始理解为何要这样做,最后是每项方法的具体展开(未完待续)

这部分都不是死知识,可能哪天我又会有更广阔的思路与解决办法,或许会推翻现在的结论,所以本文会持续保持更新。。。

RN性能优化概述

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

通过RN我们可以用JS实现跨平台App,也就是FB说的write once, run everywhere

RN为我们提供了JS的运行环境,所以前端开发者们只需要关心如何编写JS代码,画UI只需要画到virtual DOM 中,不需要特别关心具体的平台

至于如何把JS代码转成native代码的脏活累活,RN底层全干了

RN的本质是把中间的这个桥Bridge给搭好,让JS和native可以互相调用

RN的加载流程主要为几个阶段

  • 初始化RN环境
    • 创建Bridge
    • Bridge中的JS环境
    • RN模块、UI组件
  • 下载JS Bundle
  • 运行JS Bundle
  • 渲染页面

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

通过对FaceBook的ios版进行性能测试,得到上面的耗时图
可以看到,绿色的JS Init + Require占据了一大半的时间,这部分主要的操作是初始化JS环境:下载JS Bundle、运行JS Bundle

JS Bundle 是由 RN 开发工具打包出来的 JS 文件,其中不仅仅包含了RN 页面组件的 JS 代码,还有 react、react-native 的JS代码,还有我们经常会用上的redux、react-navigation等的代码,RN 非常简单的 demo 页面minify 之后的 JS Bundle 文件有接近 700KB,所以 JS Bundle文件大小是性能优化的瓶颈

假设我们有一个大型App,它囊括了非常多的页面,但是在常规使用中,很多页面甚至都不会被打开,还有一些复杂的配置文件以及很少使用的功能,这些相关的代码,在App启动的时候都是不需要的,那么,我们就可以考虑通过Unbundling拆包来优化性能

关于如何减少Bundle包的大小,目前主流的方法是拆分Bundle包,把框架代码和业务代码单独出来,框架代码非常大,因此要分离出来单独前置加载,而业务代码则变成很小的JS代码单独发布,下面提供一些前人的经验链接

但在拆包之前,FB官方还提了几条在此之前更应该做好的优化点

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通用化建设与性能优化 - Web前端 腾讯IVWeb 团队社区
不愧是腾讯,主要讲了通用化建设、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>
          // 根据需要判断是否渲染该组件
        {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

上面的内容主要是关于首屏渲染速度的性能优化

那么进入App后的性能点又在哪里呢?还是回到Bridge

首先,在苹果和谷歌两位大佬的光环下,native代码在设备上的运行速度毋容置疑,而JS作为脚本语言,本来就是以快著称,也就是说两边的独立运行都很快,如此看来,性能瓶颈只会出现在两端的通信上,但两边其实不是直接通信的,而是通过Bridge做中间人,查找、调用模块、接口等操作逻辑,会产生到能让UI层明显可感知的卡顿,那么性能控制就变成了如何尽量减少Bridge所需要的逻辑。

  • UI事件响应
    这块内容都发生在Native端,以事件形式传递到JS端,只是一个触发器,不会有过度性能问题
  • UI更新
    JS是决定显示什么界面,如何样式化页面的,一般都是由JS端发起UI更新,同时向native端同步大量的数据和UI结构,这类更新会经常出现性能问题,特别是界面复杂、数据变动量大、动画复杂、变动频率高的情况
  • UI事件响应+UI更新
    如果UI更新改动不大,那么问题不大
    如果UI事件触发了UI更新,同时逻辑复杂、耗时比较长,JS端和Native端的数据同步可能会出现时间差,由此会引发性能问题

总结起来,核心的RN性能优化点就比较清晰明朗了

  • 首屏渲染优化:处理JS Bundle包大小、文件压缩、缓存
  • UI更新优化
    • 减少更新或者合并多个更新
    • 提高组件响应速度:
      • setNativeProps直接在底层更新Native组件属性(其实没有解决JS端与Native端的数据同步问题)
      • 立即执行更新回调
    • 动画优化
      • 通过使用Annimated类库,一次性把更新发送到Native端,由Native端自己负责更新
      • 把一些耗时操作放到动画与UI更新之后执行
  • 其他优化(代码层面)

[图片上传失败...(image-f00a3f-1552026313535)]

每个小点主要会按照容易实施执行的顺序来写

一、是否重新渲染——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了一下而已,就直接触发了重新渲染,试想一下,如果页面有大型数据,这会造成多大的性能浪费

加上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都会被改变
但是,页面会不会重新渲染呢?
继续看图

很好,对象的内容变了,页面也重新渲染

那么加上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)的作用类似,都是为了避免动画卡顿,具体的原因是边渲染边执行动画,或者有大量的code计算阻塞页面进程。
InteractionManager.runAfterInteractions是在动画或者操作结束后执行

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

2)requestAnimationFrame

window.requestAnimationFrame - Web API 接口 | MDN
使用requestAnimationFrame(fn)在下一帧就立即执行回调,这样就可以异步来提高组件的响应速度;

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

还有setImmediate/setTimeout(): 这个是比较原始的奔方法,很有可能影响动画的流畅度

3) setNativeProps

Direct Manipulation · React Native
通过Direct Manipulation的方式直接在底层更新了Native组件的属性,从而避免渲染组件结构和同步太多视图变化所带来的大量开销。

这样的确会带来一定的性能提升,同时也会使代码逻辑难以理清,而且并没有解决从JS侧到Native侧的数据同步开销问题。

因此这个方式官方都不再推荐,更推荐的做法是合理使用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,
  }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容