彻底弄懂 React Native 性能优化的来龙去脉

6,245 阅读10分钟

前言

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

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

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

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

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

RN性能优化概述

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

通过RN我们可以用JS实现跨平台App,也就是FB说的write once, run everywhere
74b378bb-b3f8-4faa-98a5-cb713ee86008

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

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

bb6cd988-3da0-45ef-b362-d9818b903cc5

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
ed25dbb5-e80d-4f83-a087-13291f2cd48d

通过对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更新之后执行
  • 其他优化(代码层面)

c6c076d9-2dae-4c56-b382-83b7f268814a

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

一、是否重新渲染——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 })
2018-04-30 16 59 13

明明没有改变a,只是setState了一下而已,就直接触发了重新渲染,试想一下,如果页面有大型数据,这会造成多大的性能浪费

加上shouldComponentUpdate钩子看看如何

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

2018-04-30 17 00 54

嗯,这下好了点,不会无脑渲染了

那么假如是个引用对象呢?

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都会被改变
但是,页面会不会重新渲染呢?
继续看图
2018-04-30 17 15 49

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

那么加上shouldComponentUpdate比较一下呢?

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

2018-04-30 17 17 51

页面毫无变化
原因: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而已,没什么特殊的
image

二、组件响应速度(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
2018-06-11 10 26 44

LayoutAnimation效果1
2018-06-11 09 58 06
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,
  }
};

(未完待续。。。)

后记

感谢您耐心看到这里,希望有所收获!

如果不是很忙的话,麻烦点个star⭐【Github博客传送门】,举手之劳,却是对作者莫大的鼓励。

我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步。