React-Native国际化多语言

使用 React Native 替代基于 WebView 的框架来开发 App 的一个强有力的理由,就是为了使 App 可以达到每秒 60
帧(足够流畅),并且能有类似原生 App 的外观和手感。因此我们也尽可能地优化 React Native 去实现这一目标,使开发者能集中精力处理 App
的业务逻辑,而不用费心考虑性能。但是,总还是有一些地方有所欠缺,以及在某些场合 React Native
还不能够替你决定如何进行优化(用原生代码写也无法避免),因此人工的干预依然是必要的。

本文的目的是教给你一些基本的知识,来帮你排查性能方面的问题,以及探讨这些问题产生的原因和推荐的解决方法。

关于“帧”你所需要知道的

老一辈人常常把电影称为“移动的画”,是因为视频中逼真的动态效果其实是一种幻觉,这种幻觉是由一组静态的图片以一个稳定的速度快速变化所产生的。我们把这组图片中的每一张图片叫做一帧,而每秒钟显示的帧数直接的影响了视频(或者说用户界面)的流畅度和真实感。iOS
设备提供了每秒 60 的帧率,这就留给了开发者和 UI 系统大约 16.67ms 来完成生成一张静态图片(帧)所需要的所有工作。如果在这分派的
16.67ms 之内没有能够完成这些工作,就会引发‘丢帧’的后果,使界面表现的不够流畅。

下面要讲的事情可能更为复杂:请先调出你应用的开发菜单,打开Show FPS Monitor. 你会注意到有两个不同的帧率.

JS 帧率(JavaScript 线程)

对大多数 React Native 应用来说,业务逻辑是运行在 JavaScript 线程上的。这是 React 应用所在的线程,也是发生 API
调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。如果
JavaScript 线程有一帧没有及时响应,就被认为发生了一次丢帧。
例如,你在一个复杂应用的根组件上调用了this.setState,从而导致一次开销很大的子组件树的重绘,可想而知,这可能会花费 200ms 也就是整整 12
帧的丢失。此时,任何由 JavaScript 控制的动画都会卡住。只要卡顿超过 100ms,用户就会明显的感觉到。

这种情况经常发生在老的Navigator导航器的切换过程中:当你 push 一个新的路由时,JavaScript
需要绘制新场景所需的所有组件,以发送正确的命令给原生端去创建视图。由于切换是由 JavaScript
线程所控制,因此经常会占用若干帧的时间,引起一些卡顿。有的时候,组件会在componentDidMount函数中做一些额外的事情,这甚至可能会导致页面切换过程中多达一秒的卡顿。

另一个例子是老的触摸事件的响应:如果你正在 JavaScript
线程处理一个跨越多个帧的工作,你可能会注意到TouchableOpacity的响应被延迟了。这是因为 JavaScript
线程太忙了,不能够处理主线程发送过来的原始触摸事件,结果TouchableOpacity就不能及时响应这些事件并命令主线程的页面去调整透明度了。

UI 帧率(主线程)

很多人会注意到,NavigatorIOS的性能要比老的纯 JS 实现的Navigator好的多。原因就是它的切换动画是完全在主线程上执行的,因此不会被
JavaScript 线程上的掉帧所影响。

同样,当 JavaScript
线程卡住的时候,你仍然可以欢快的上下滚动ScrollView,因为ScrollView运行在主线程之上(尽管滚动事件会被分发到 JS
线程,但是接收这些事件对于滚动这个动作来说并不必要)。

性能问题的常见原因

开发模式 (dev=true)

JavaScript
线程的性能在开发模式下是很糟糕的。这是不可避免的,因为有许多工作需要在运行的时候去做,譬如使你获得良好的警告和错误信息,又比如验证属性类型(propTypes)以及产生各种其他的警告。请务必注意在[release
模式](https://reactnative.cn/docs/running-on-
device#%E5%8F%91%E5%B8%83%E5%BA%94%E7%94%A8)下去测试性能。

console.log 语句

在运行打好了离线包的应用时,控制台打印语句可能会极大地拖累 JavaScript 线程。注意有些第三方调试库也可能包含控制台打印语句,比如[redux-
logger](https://github.com/evgenyrodionov/redux-
logger),所以在发布应用前请务必仔细检查,确保全部移除。

这里有个小技巧可以在发布时屏蔽掉所有的console.*调用。React Native
中有一个全局变量DEV用于指示当前运行环境是否是开发环境。我们可以据此在正式环境中替换掉系统原先的 console 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if (!__DEV__) {



  global.console = {



    info: () = {},



    log: () = {},



    warn: () = {},



    debug: () = {},



    error: () = {}



  };



}

这样在打包发布时,所有的控制台语句就会被自动替换为空函数,而在调试时它们仍然会被正常调用。

还有个[babel 插件](https://babeljs.io/docs/plugins/transform-remove-
console/)可以帮你移除所有的console.*调用。首先需要使用yarn add –dev babel-plugin-transform-
remove-console来安装,然后在项目根目录下编辑(或者是新建)一个名为·.babelrc`的文件,在其中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27


{



  "env": {



    "production": {



      "plugins": ["transform-remove-console"]



    }



  }



}

这样在打包发布时,所有的控制台语句就会被自动移除,而在调试时它们仍然会被正常调用。

ListViewinitial rendering is too slow or scroll performance is bad for

large lists

Use the
newFlatListorSectionListcomponent
instead. Besides simplifying the API, the new list components also have
significant performance enhancements, the main one being nearly constant
memory usage for any number of rows.

If yourFlatListis rendering slow, be
sure that you’ve implemented[getItemLayout](https://facebook.github.io/react-
native/flatlist.md#getitemlayout)to optimize rendering speed by skipping
measurement of the rendered items.

在重绘一个几乎没有什么变化的页面时,JS 帧率严重降低

你可以实现shouldComponentUpdate函数来指明在什么样的确切条件下,你希望这个组件得到重绘。如果你编写的是纯粹的组件(界面完全由 props
和 state
所决定),你可以利用PureComponent来为你做这个工作。再强调一次,不可变的数据结构(immutable,即对于引用类型数据,不修改原值,而是复制后修改并返回新值)在提速方面非常有用
—— 当你不得不对一个长列表对象做一个深度的比较,它会使重绘你的整个组件更加快速,而且代码量更少。

在屏幕上移动视图(滚动,切换,旋转)时,UI 线程掉帧

当具有透明背景的文本位于一张图片上时,或者在每帧重绘视图时需要用到透明合成的任何其他情况下,这种现象尤为明显。设置shouldRasterizeIOS或者renderToHardwareTextureAndroid属性可以显著改善这一现象。
注意不要过度使用该特性,否则你的内存使用量将会飞涨。在使用时,要评估你的性能和内存使用情况。如果你没有需要移动这个视图的需求,请关闭这一属性。

使用动画改变图片的尺寸时,UI 线程掉帧

在 iOS 上,每次调整 Image
组件的宽度或者高度,都需要重新裁剪和缩放原始图片。这个操作开销会非常大,尤其是大的图片。比起直接修改尺寸,更好的方案是使用transform:
[{scale}]的样式属性来改变尺寸。比如当你点击一个图片,要将它放大到全屏的时候,就可以使用这个属性。

Touchable 系列组件不能很好的响应

有些时候,如果我们有一项操作与点击事件所带来的透明度改变或者高亮效果发生在同一帧中,那么有可能在onPress函数结束之前我们都看不到这些效果。比如在onPress执行了一个setState的操作,这个操作需要大量计算工作并且导致了掉帧。对此的一个解决方案是将onPress处理函数中的操作封装到requestAnimationFrame中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
handleOnPress() {



  // 谨记在使用requestAnimationFrame、setTimeout以及setInterval时



  // 要使用TimerMixin(其作用是在组件unmount时,清除所有定时器)



  this.requestAnimationFrame(() = {



    this.doExpensiveAction();



  });



}

分析

你可以利用内置的分析器来同时获取 JavaScript 线程和主线程中代码执行情况的详细信息。

对于 iOS 来说,Instruments 是一个宝贵的工具库,Android 的话可以使用 systrace,具体可以参考下面的使用 systrace
调试 Android UI 性能。

But first,[make sure that Development Mode is
OFF!](https://reactnative.cn/docs/performance#running-in-development-mode-dev-
true)You should see__DEV__ === false, development-level warning are OFF,
performance optimizations are ONin your application logs.

Another way to profile JavaScript is to use the Chrome profiler while
debugging. This won’t give you accurate results as the code is running in
Chrome but will give you a general idea of where bottlenecks might be. Run the
profiler under Chrome’sPerformancetab. A flame graph will appear underUser
Timing. To view more details in tabular format, click at theBottom Uptab below
and then selectDedicatedWorker Threadat the top left menu.

使用 systrace 调试 Android UI 性能

Android supports 10k+ different phones and is generalized to support software
rendering: the framework architecture and need to generalize across many
hardware targets unfortunately means you get less for free relative to iOS.
But sometimes, there are things you can improve – and many times it’s not
native code’s fault at all!

The first step for debugging this jank is to answer the fundamental question
of where your time is being spent during each 16ms frame. For that, we’ll be
using a standard Android profiling tool calledsystrace.

systraceis a standard Android marker-based profiling tool (and is installed
when you install the Android platform-tools package). Profiled code blocks are
surrounded by start/end markers which are then visualized in a colorful chart
format. Both the Android SDK and React Native framework provide standard
markers that you can visualize.

1. Collecting a trace

First, connect a device that exhibits the stuttering you want to investigate
to your computer via USB and get it to the point right before the
navigation/animation you want to profile. Runsystraceas follows:

$ <path_to_android_sdk/platform-tools/systrace/systrace.py –time=10 -o
trace.html sched gfx view -a <your_package_name

A quick breakdown of this command:

timeis the length of time the trace will be collected in seconds

sched,gfx, andvieware the android SDK tags (collections of markers) we care
about:schedgives you information about what’s running on each core of your
phone,gfxgives you graphics info such as frame boundaries, andviewgives you
information about measure, layout, and draw passes

-a <your_package_nameenables app-specific markers, specifically the ones built into the React Native framework.your_package_namecan be found in theAndroidManifest.xmlof your app and looks likecom.example.app

Once the trace starts collecting, perform the animation or interaction you
care about. At the end of the trace, systrace will give you a link to the
trace which you can open in your browser.

2. Reading the trace

After opening the trace in your browser (preferably Chrome), you should see
something like this:

HINT: Use the WASD keys to strafe and zoom

If your trace .html file isn’t opening correctly, check your browser console
for the following:

SinceObject.observewas deprecated in recent browsers, you may have to open the
file from the Google Chrome Tracing tool. You can do so by:

Opening tab in
chromechrome://tracing

Selecting load

Selecting the html file generated from the previous command.

Enable VSync highlighting

Check this checkbox at the top right of the screen to highlight the 16ms
frame boundaries:

You should see zebra stripes as in the screenshot above. If you don’t, try
profiling on a different device: Samsung has been known to have issues
displaying vsyncs while the Nexus series is generally pretty reliable.

3. Find your process

Scroll until you see (part of) the name of your package. In this case, I was
profilingcom.facebook.adsmanager, which shows up asbook.adsmanagerbecause of
silly thread name limits in the kernel.

On the left side, you’ll see a set of threads which correspond to the timeline
rows on the right. There are a few threads we care about for our purposes: the
UI thread (which has your package name or the name UI Thread),mqt_js,
andmqt_native_modules. If you’re running on Android 5+, we also care about the
Render Thread.

UI Thread.This is where standard android measure/layout/draw happens. The
thread name on the right will be your package name (in my case
book.adsmanager) or UI Thread. The events that you see on this thread should
look something like this and have to do withChoreographer,traversals,
andDispatchUI:

JS Thread.This is where JavaScript is executed. The thread name will be
eithermqt_jsor<…depending on how cooperative the kernel on your device is
being. To identify it if it doesn’t have a name, look for things
likeJSCall,Bridge.executeJSCall, etc:

Native Modules Thread.This is where native module calls (e.g. theUIManager)
are executed. The thread name will be eithermqt_native_modulesor<…. To
identify it in the latter case, look for things
likeNativeCall,callJavaModuleMethod, andonBatchComplete:

Bonus: Render Thread.If you’re using Android L (5.0) and up, you will also
have a render thread in your application. This thread generates the actual
OpenGL commands used to draw your UI. The thread name will be
eitherRenderThreador<…. To identify it in the latter case, look for things
likeDrawFrameandqueueBuffer:

Identifying a culprit

A smooth animation should look something like the following:

Each change in color is a frame – remember that in order to display a frame,
all our UI work needs to be done by the end of that 16ms period. Notice that
no thread is working close to the frame boundary. An application rendering
like this is rendering at 60 FPS.

If you noticed chop, however, you might see something like this:

Notice that the JS thread is executing basically all the time, and across
frame boundaries! This app is not rendering at 60 FPS. In this case,the
problem lies in JS.

You might also see something like this:

In this case, the UI and render threads are the ones that have work crossing
frame boundaries. The UI that we’re trying to render on each frame is
requiring too much work to be done. In this case,the problem lies in the
native views being rendered.

At this point, you’ll have some very helpful information to inform your next
steps.

Resolving JavaScript issues

If you identified a JS problem, look for clues in the specific JS that you’re
executing. In the scenario above, we seeRCTEventEmitterbeing called multiple
times per frame. Here’s a zoom-in of the JS thread from the trace above:

This doesn’t seem right. Why is it being called so often? Are they actually
different events? The answers to these questions will probably depend on your
product code. And many times, you’ll want to look
into[shouldComponentUpdate](https://facebook.github.io/react/component-
specs.md#updating-shouldcomponentupdate).

Resolving native UI Issues

If you identified a native UI problem, there are usually two scenarios:

the UI you’re trying to draw each frame involves too much work on the GPU, or

You’re constructing new UI during the animation/interaction (e.g. loading in
new content during a scroll).

Too much GPU work

In the first scenario, you’ll see a trace that has the UI thread and/or Render
Thread looking like this:

Notice the long amount of time spent inDrawFramethat crosses frame boundaries.
This is time spent waiting for the GPU to drain its command buffer from the
previous frame.

To mitigate this, you should:

investigate usingrenderToHardwareTextureAndroidfor complex, static content
that is being animated/transformed (e.g. theNavigatorslide/alpha animations)

make sure that you arenotusingneedsOffscreenAlphaCompositing, which is
disabled by default, as it greatly increases the per-frame load on the GPU in
most cases.

If these don’t help and you want to dig deeper into what the GPU is actually
doing, you can check outTracer for OpenGL
ES
.

Creating new views on the UI thread

In the second scenario, you’ll see something more like this:

Notice that first the JS thread thinks for a bit, then you see some work done
on the native modules thread, followed by an expensive traversal on the UI
thread.

There isn’t an easy way to mitigate this unless you’re able to postpone
creating new UI until after the interaction, or you are able to simplify the
UI you’re creating. The react native team is working on an infrastructure
level solution for this that will allow new UI to be created and configured
off the main thread, allowing the interaction to continue smoothly.

拆包(RAM bundles)和内联引用

如果你有一个较为庞大的应用程序,你可能要考虑使用RAM(Random Access Modules,随机存取模块)格式的 bundle
和内联引用。这对于具有大量页面的应用程序是非常有用的,这些页面在应用程序的典型使用过程中可能不会被打开。通常对于启动后一段时间内不需要大量代码的应用程序来说是非常有用的。例如应用程序包含复杂的配置文件屏幕或较少使用的功能,但大多数会话只涉及访问应用程序的主屏幕更新。我们可以通过使用RAM格式来优化bundle的加载,并且内联引用这些功能和页面(当它们被实际使用时)。

加载 JavaScript

在 react-native 执行 JS 代码之前,必须将代码加载到内存中并进行解析。如果你加载了一个 50MB 的 bundle,那么所有的 50mb
都必须被加载和解析才能被执行。RAM 格式的 bundle 则对此进行了优化,即启动时只加载 50MB 中实际需要的部分,之后再逐渐按需加载更多的包。

内联引用

内联引用(require 代替 import)可以延迟模块或文件的加载,直到实际需要该文件。一个基本的例子看起来像这样:

优化前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 <TextVery Expensive Component</Text;



  }



}

优化后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import React, { Component } from 'react';



import { TouchableOpacity, View, Text } from 'react-native';



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}>



          <TextLoad></Text>



        </TouchableOpacity>



        {this.state.needsExpensive ? <VeryExpensive /> : null}



      </View>



    );



  }



}

即便不使用 RAM 格式,内联引用也会使启动时间减少,因为优化后的代码只有在第一次 require 时才会执行。

启用 RAM 格式

在 iOS 上使用 RAM 格式将创建一个简单的索引文件,React Native 将根据此文件一次加载一个模块。在 Android
上,默认情况下它会为每个模块创建一组文件。你可以像 iOS 一样,强制 Android 只创建一个文件,但使用多个文件可以提高性能,并降低内存占用。

在 Xcode 中启用 RAM 格式,需要编辑 build phase 里的”Bundle React Native code and
images”。在../node_modules/react-native/packager/react-native-xcode.sh中添加export
BUNDLE_COMMAND=”ram-bundle”:

export BUNDLE_COMMAND=”ram-bundle”

export NODE_BINARY=node

../node_modules/react-native/packager/react-native-xcode.sh

在 Android 上启用 RAM 格式,需要编辑 android/app/build.gradle 文件。在apply from:
“../../node_modules/react-native/react.gradle”之前修改或添加project.ext.react:

1
2
3
4
5
6
7
8
9
project.ext.react = [



  bundleCommand: "ram-bundle",



]

如果在 Android 上,你想使用单个索引文件(如前所述),请在 Android 上使用以下行:

1
2
3
4
5
6
7
8
9
10
11
12
13
project.ext.react = [



  bundleCommand: "ram-bundle",



  extraPackagerArgs: ["--indexed-ram-bundle"]



]

配置预加载及内联引用

现在我们已经启用了RAM格式,然而调用require会造成额外的开销。因为当遇到尚未加载的模块时,require需要通过bridge来发送消息。这主要会影响到启动速度,因为在应用程序加载初始模块时可能触发相当大量的请求调用。幸运的是,我们可以配置一部分模块进行预加载。为了做到这一点,你将需要实现某种形式的内联引用。

添加 packager 配置文件

在项目中创建一个名为 packager 的文件夹,并创建一个名为 config.js 的文件。添加以下内容:

cons

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
t config = {



  transformer: {



    getTransformOptions: () = {



      return {



        transform: { inlineRequires: true },



      };



    },



  },



};



module.exports = config;

在 Xcode 的 Build phase 中添加export BUNDLE_CONFIG=”packager/config.js”

1
2
3
4
5
6
7
8
9
export BUNDLE_COMMAND="ram-bundle"



export BUNDLE_CONFIG="packager/config.js"



export NODE_BINARY=node

../node_modules/react-native/packager/react-native-xcode.sh

编辑 android/app/build.gradle 文件,添加bundleConfig: “packager/config.js”,

1
2
3
4
5
6
7
8
9
10
11
12
13
project.ext.react = [



  bundleCommand: "ram-bundle",



  bundleConfig: "packager/config.js"



]

最后,在 package.json 的“scripts”下修改“start”命令来启用配置文件:

“start”: “node node_modules/react-native/local-cli/cli.js start –config
../../../../packager/config.js”,

此时用npm start启动你的 packager 服务即会加载配置文件。请注意,如果你仍然通过 xcode 或是 react-native run-
android 等方式自动启动 packager 服务,则由于没有使用上面的参数,不会加载配置文件。

调试预加载的模块

在您的根文件 (index.(ios|android).js) 中,您可以在初始导入(initial imports)之后添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const modules = require.getModules();



const moduleIds = Object.keys(modules);



const loadedModuleNames = moduleIds



  .filter(moduleId = modules[moduleId].isInitialized)



  .map(moduleId = modules[moduleId].verboseName);



const waitingModuleNames = moduleIds



  .filter(moduleId = !modules[moduleId].isInitialized)



  .map(moduleId = modules[moduleId].verboseName);



// make sure that the modules you expect to be waiting are actually waiting



console.log(



  'loaded:',



  loadedModuleNames.length,



  'waiting:',



  waitingModuleNames.length



);



// grab this text blob, and put it in a file named packager/modulePaths.js



console.log(`module.exports =
${JSON.stringify(loadedModuleNames.sort())};`);

当你运行你的应用程序时,你可以查看 console 控制台,有多少模块已经加载,有多少模块在等待。你可能想查看
moduleNames,看看是否有任何意外。注意在首次 import
时调用的内联引用。你可能需要检查和重构,以确保只有你想要的模块在启动时加载。请注意,您可以根据需要修改 Systrace 对象,以帮助调试有问题的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require.Systrace.beginEvent = (message) = {



  if(message.includes(problematicModule)) {



    throw new Error();



  }



}

虽然每个 App 各有不同,但只加载第一个页面所需的模块是有普适意义的。当你满意时,把 loadedModuleNames 的输出放到
packager/modulePaths.js 文件中。

更新配置文件

Returning to packager/config.js we should update it to use our newly generated
modulePaths.js file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const modulePaths = require('./modulePaths');



const resolve = require('path').resolve;



const fs = require('fs');



// Update the following line if the root folder of your app is somewhere
else.



const ROOT_FOLDER = path.resolve(__dirname, '..');



const config = {



  transformer: {



    getTransformOptions: () = {



      const moduleMap = {};



      modulePaths.forEach(path = {



        if (fs.existsSync(path)) {



          moduleMap[resolve(path)] = true;



        }



      });



      return {



        preloadedModules: moduleMap,



        transform: { inlineRequires: { blacklist: moduleMap } },



      };



    },



  },



};



module.exports = config;

在启用RAM格式之后,配置文件中的preloadedModules条目指示哪些模块需要预加载。当 bundle 被加载时,这些模块立即被加载,甚至在任何
requires 执行之前。blacklist 表明这些模块不应该被要求内联引用,因为它们是预加载的,所以使用内联没有性能优势。实际上每次解析内联引用
JavaScript 都会花费额外的时间。

测试和衡量改进

您现在应该准备好使用RAM格式和内联引用来构建您的应用了。保存启动前后的时间,来测试下有多少改进吧!

无状态组件需使用 PureComponent 而不是 Component; 说明:无状态组件是指内部没有使用 state 的组件,但是可以使用 props
来进行某些属性控制;

使用 InteractionManager.runAfterInteractions,在动画或者某些特定场景中利用 InteractionManager
来选择性的渲染新场景所需的最小限度的内容; 使用场景类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
class ExpensiveScene extends [React.Component](http://react.component/){



    constructor(props, context) {



        super(props, context);



        this.state = { renderPlaceholderOnly: true };



    }



    componentDidMount() {



        InteractionManager.runAfterInteractions(() = {



            this.setState({ renderPlaceholderOnly: false });



        });



    }



    render() {



        if (this.state.renderPlaceholderOnly) {



            return this.renderPlaceholderView();



        }



        return (



            <View



                <TextYour full view goes here</Text



            </View



        );



    }



    renderPlaceholderView() {



        return (



            <View



                <TextLoading...</Text



            </View



        );



    }



}

使用新版本组件替换旧办法组件; 例如:FlatList 替换 ListView,React Navigation 替换 Navigator 等

在使用 Touchable 系列组件时,进行 setState 或者大量调帧操作,请使用如下方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
handleOnPress() {



    this.requestAnimationFrame(() = {



      //todo



    });



  }

React-Native-bundle拆分与合并之安卓篇

在网上看到携程之前拆分的一些经验

先来说一组数据,一个Helloorld的App,如果使用0.30 RN 官方命令react-native
bundle打包出来的JSBundle文件大小大约为531KB,RN框架JavaScript本身占了530KB, zip压缩之后也有148KB。

如果只有一两个业务使用,这点大小算不了什么,但是对于我们这种动辄几十个业务的场景,如果每个业务的JSBundle都需要这么大的一个RN框架本身,那将是不可接受的。

因此,我们需要对RN官方的打包脚本做改造,将框架代码拆分出来,让所有业务使用一份框架代码。

开始拆分之前, 我们先以HelloWorld的RNApp为基础介绍几个背景知识。

上述是一个HelloWorld RNApp代码的结构,基本分为3部分

头部:各依赖模块引用部分;

中间:入口模块和各业务模块定义部分;

尾部:入口模块注册部分;

上述是HelloWorld RNApp打包之后JSBundle文件的结构,基本分为3部分
头部:全局定义,主要是define,require等全局模块的定义; 中间:模块定义,RN框架和业务的各个模块定义; 尾部:引擎初始化和入口函数执行;

d是RN自定义的define,符合CommonJS规范d后面的数字是模块的id,是在RN打包过程中,解析依赖关系,自增长生成的。

如果所有业务代码,都遵照一个规则:入口JS文件首先require的都是react/react-native, 则打包生成的JSBundle里面react
/react-native相关的模块id都是固定的。

拆分方案一

基于上面2点背景知识介绍,我们很容易发现,如果将打包之后的JSBundle文件,拆分成2部分(框架部分+业务模块部分),使用的时候合并起来,然后去加载,即可实现拆分功能。

具体实现步骤:

创建一个空工程,入口文件只需要2行代码,require react/react-native即可;

使用react-native bundle命令,打包该入口文件,生成common.js;

使用react-native bundle打包业务工程(有一点要保证,业务工程入口文件前面2行代码也是require react/react-
native), 生成business_all.js;

开发工具,从business_all.js里面删除common.js的内容,剩下的就是business.js;

App加载的时候将common.js和business.js合并在一起,然后加载;

貌似功能完成,可是回到Dive into React Native performance,
这么做还是优化不了JSBundle的执行时间,因为我们不能把拆分开的2个文件分别执行,因为加载common.js会提示找不到RNApp的入口,先执行business.js,会提示一堆依赖的RN模块找不到。

显然,这种拆分方式不能满足我们这种需要。

那这个方案就完全没有价值吗?不是的,如果你做的是一个纯RNApp,native只是一个壳,里面业务全是RN开发的,完全可以使用这种方式做拆分,这种方案简单,无侵入,实现成本低,不需要修改任何RN打包代码和RN
Runtime代码。

拆分方案二

RN框架部分文件(common.js)大小530KB,如此大的js文件,占用了绝大部分的JS执行时间,这块时间如果能放到后台预先做完,进入业务也只需执行业务页面的几个JS文件,将可以大大提升页面加载速度,参考上面的RN性能瓶颈图,预估可以提升100%。

按照这个思路,能后台加载的JS文件, 实际上是就是一个RNApp,因此
我们设计了一个空白页面的FakeApp,这个FakeApp做一件事情,就是监听要显示的真实的业务JS模块,收到监听之后,渲染业务模块,显示页面。

FakeApp设计如下:

为了实现该拆包方案,需要改造react-native的打包命令;

基于FakeApp打common.js包的时候, 需要记录RN各个模块名和模块id之间的mapping关系;

打业务模块包的时候,判断,如果已经在mapping文件里面的模块,不要打包到业务包中

改造页面加载流程:

因为要能够后台加载,所以需分离UI和JS加载引擎<iOS-RCTBridge, Android-ReactInstanceManager;

进入业务RN页面时候,获取预加载好的JS引擎,然后发送消息给FakeApp,告知该渲染的业务JS模块;

通过后台预加载,省去了绝大部分的JS加载时间,似乎问题已经完美解决。

但是,如果随着业务不断膨胀,一个RN业务JS代码也达到500KB,进入这个业务页面,500多KB JS文件读取出来,执行,整个JS执行的时间瓶颈会再次出现。

拆分方案三

正在此时,我们研究RN在Facebook App里面的使用情况,发现了Unbundle,简单点说,就是将所有的JS模块都拆分成独立的文件。

下面截图就是unbundle打包的文件格式:

entry.js就是global部分定义+RNApp入口;

UNBUNDLE文件是用于标识这是一个unbundle包的flag;

12.js,13.js就是各个模块,文件名就是模块id;

在业务执行,需要加载模块(require)的时候,就去磁盘查找该文件,读取、执行。

RN里面加载模块流程说明,以require(66666)模块为例:

首先从__d<就是前文提到的define的缓存列表里面查找是否有定义过模块66666,如果有,直接返回,如果没有走到下面第二步的nativeRequire;

nativeRequire根据模块id,查找文件所在路径,读取文件内容;

定义模块,_d(66666)=eval(JS文件内容),会将这个模块ID和JS代码执行结果记录在define的缓存列表里面;

打包通过react-native unbundle 命令,可以给android平台打出这样的unbundle包。

顺便提一下,这个unbundle方案,只在android上有效,打ios平台的unbundle包,是打不出来的,在RN的打包脚本上有一行注释,大致意思是在iOS上众多小文件读取,文件IO效率不够高,android上没这样的问题,然后判断如果是打iOS的unbundle包的时候,直接return了。

相对应的,iOS开发了一个prepack的打包模式,简单点说,就是把所有的JS模块打包到一个文件里面,打包成一个二进制文件,并固定0xFB0BD1E5为文件开始
,这个二进制文件里面有个meta-
table,记录各个模块在文件中的相对位置,在加载模块(require)的时候,通过fseek,找到相应的文件开始,读取,执行。

在Unbundle的启发下,我们修改打包工具,开发了CRNUnbunle,做了简单的优化,把众多零散的JS文件做了简单的合并。

将common部分的JS文件,合并成一个common_ios(android).js.

_crn_config记录了这个RNApp的入口模块ID以及其他配置信息,详见下图:

main_module为当前业务模块入口模块ID;

module_path为业务模块JS文件所在当前包的相对路径;

666666=0.js,说明666666这个模块在0.js文件里面;

做完这个拆包和加载优化之后,我们用自己的几个业务做了下测试,下图是当时的测试验证数据。

可以看出,iOS和android基本都比官方打包方式的加载时间,减少了50%。

这是自己单机测试的数据,那上线之后,数据如何呢?

下图,是我们分析一天的数据,得出的平均值<排除掉了5s以上的异常数据,后面实测下来5s以上数据极少;

看到这个数据,发现和我们自己测试的基本一致,但是还有一个疑问,加载的时间分布,是否服从正态分布,会不会很离散,快的设备很快,慢的设备很慢呢?

然后我又进一步分析这一天的数据,按照页面加载时间区间分布统计。

看图上数据,很明显,iOS&Android基本一致,将近98%的用户都能在1s内加载完成页面,符合我们期望的正态分布,所以bundle拆分到此基本完成。

实践

我先用bundle打包命令打一个bundle出来

react-nativebundle –platform android –devfalse–entry-file index.android.js
–bundle-output finalbundle/index.android.bundle –assets-dest finalbundle/

只有一个简单的3k左右的index.android.js,打出了一个五百多k的index.android.bundle,看看里面是些什么

密密麻麻但又有规则

!function打头的是公共的头部部分

_d(function是JS文件,用ctrl+s搜索welcome,找到我们的index.android.js,原来是在第一行的_d(function,而且结尾有个参数0,其余部分其实都是公共的js

;require(120),是基础文件的配置入口,require(0)则是业务的入口

基于以上,能想到一个办法:

内置一个common.js文件,里面包含了bundle文件公共部分的代码,

业务代码单独生成一个js文件

在需要展示加载某一个页面的时,将common.js和当前页面需要加载的业务js合并,然后再加载

这个办法解决了一部分问题,但加载时还是一个整体。如果common部分能重用,就能大大提升效率。所以就来试试上面提到的unbundle命令

react-nativeunbundle –platform android –devfalse–entry-file
index.android.js –bundle-output build/index.android.bundle

生成的bundle只有14行了

但多了一个js-
modules文件夹,里面的xx.js里面的内容就是将之前的__d(xx)抽出来单独放到一个文件里面,通过require(xx)加载到内存供调用

基于unbundle命令再设计一个上面提到的fake页面用来加载相应的业务模块,这个页面可以预先在后台初始化js引擎,将公共部分的common.js文件读取到内存,然后设置一个监听事件,通过emmit方式,当需要加载某个页面的的module的时候讲这个页面的module的id传递过来,然后通过require方法调用这个模块。

思路差不多是这样了,来试试看实现起来有没什么坑。

首先

我拿例子跑了一下,瞬间明白了流程是怎么回事,有几个关键:

DeviceEventEmitter

前端发起监听,后端需要用的时候调用emit触发,通过返回模块id,然后return
React.createElement(返回的模块ID,this.props)即可定制加载

配置文件

这个配置文件之前不是很理解为什么好多等于0.js、等于1.js,现在明白其实就是不同bu的入口JS,因为都是单页路由的形式,不过这个配置其实是一套打包的一个流程,不在这里做,以后研究打包工具的时候加上。

然后

我试着把这样融入到之前的demo里。

先建两个test页面,用于测试切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
importReact, { Component }from'react';



import{



AppRegistry,



StyleSheet,



Text,



View,



}from'react-native';



classtesteightextendsComponent{



render() {



return(



<Viewstyle={styles.container}



<Textstyle={styles.welcome}



Welcome to Test 8888



</Text



</View



);



}



}



conststyles = StyleSheet.create({



welcome: {



fontSize:20,



textAlign:'center',



margin:10,



}



});



module.exports = testeight;

然后在index.android.js加入切换按钮

/**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397




* Sample React Native App



* https://github.com/facebook/react-native



* @flow



*/



importReact, { Component }from'react';



import{



AppRegistry,



StyleSheet,



Text,



View,



Image,



NativeModules,



DeviceEventEmitter,



}from'react-native';



exportdefaultclassAwesomeProjectextendsComponent{



constructor(props){



super(props);



this.state = {



content:null,showModule:false



};



DeviceEventEmitter.addListener("test", (result) = {



letmainComponent =require(result.name);



this.setState({



content:mainComponent,



showModule:true



})



});



}



render() {



let_content =null;



if(this.state.content){



_content = React.createElement(this.state.content,this.props);



return_content;



}else{



return(



<Viewstyle={styles.container}>



<Textstyle={styles.welcome}>



Welcome to React Native!



</Text>



<Textstyle={styles.instructions}>



To get started, edit index.android.js



</Text>



<Textstyle={styles.instructions}>



Double tap R on your keyboard to reload,{'\n'}



Shake or press menu button for dev menu



</Text>



<Textstyle={styles.instructions}onPress={()=this.showToast()}>



点我调用原生



</Text>



<Textstyle={styles.instructions}onPress={()=this.updateBundle()}>



点我更新bundle



</Text>



<Textstyle={styles.instructions}onPress={()=this.goNine()}>



点我加载页面9999



</Text>



<Textstyle={styles.instructions}onPress={()=this.goEight()}>



点我加载页面8888



</Text>



<Image>



source={require('./img/music_play.png')}



style={{width:92,height:92}}



/



</View>



);



}



}



updateBundle () {



NativeModules.updateBundle.check("5.0.0");



}



showToast () {



//调用原生



NativeModules.RNToastAndroid.show('from native',100);



}



goNine () {



NativeModules.BundleLoad.goPage(9999);



}



goEight () {



NativeModules.BundleLoad.goPage(8888);



}



}



const styles = StyleSheet.create({



container: {



flex: 1,



justifyContent: 'center',



alignItems: 'center',



backgroundColor: '#F5FCFF',



},



welcome: {



fontSize: 20,



textAlign: 'center',



margin: 10,



},



instructions: {



textAlign: 'center',



color: '#333333',



marginBottom: 5,



},



});



AppRegistry.registerComponent('rnandnative', () = AwesomeProject);

然后把index.android和两个test页面都用unbundle打包

1
2
3
4
5
6
7
8
9
10
11
12
react-nativeunbundle --platform android --devfalse--entry-file
index.android.js --bundle-output unbundle/index.android.bundle



react-nativeunbundle --platform android --devfalse--entry-file
bundletest1.js --bundle-output unbundle/index.android.bundle1



react-nativeunbundle --platform android --devfalse--entry-file
bundletest2.js --bundle-output unbundle/index.android.bundle2

然后把index.android.bundle1、index.android.bundle2中除了_d的那句打头的去掉,把__d(0的0改为9999、8888,把文件名改为9999.js和8888
.js丢到js-modules里,这个讲的估计不是很明白,但去看看代码就懂了。

然后建一个触发emit的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class RNBundleLoadModule extends ReactContextBaseJavaModule{



private ReactApplicationContext reactApplicationContext;



public RNBundleLoadModule(ReactApplicationContext reactApplicationContext){



super(reactApplicationContext);



}



@Override



public String  getName(){



return "BundleLoad";



}



@ReactMethod



public void goPage(final Integer pageid){



System.out.print("########"+pageid+"########");



// failedCallback.invoke();



WritableMap params = Arguments.createMap();



params.putInt("name", pageid);



reactApplicationContext



.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)



.emit("test", params);



}



}

跑起来,一切OK。

参考

https://github.com/pukaicom/reactNativeBundleBreak

React-Native-DeviceEventEmitter

实现

DeviceEventEmitter在RN内的发送和接受消息。例如:

A页面注册通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import {DeviceEventEmitter} from'react-native';



//…



//调用事件通知



DeviceEventEmitter.emit('xxxName’,param);



//xxxName:通知的名称 param:发送的消息(传参)

B页面接收通知:

componentDidMount(){



varself =this;



this.listener =DeviceEventEmitter.addListener('xxxName',function(param){



// use param do something



});



}



//xxxName:通知的名称 param:接收到的消息(传参)



componentWillUnmount(){



this.listener.remove();



}



//在componentWillUnmount 内需要我们手动移除通知

知道DeviceEventEmitter的简单使用后

我的页面在获取到用户数据后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//注册监听事件,时间名称:changeMine 传参:jsonData.avatar(头像url)



DeviceEventEmitter.emit('changeMine',jsonData.avatar);

tabbar.js 文件:

componentDidMount(){



varself =this;



this.listener = DeviceEventEmitter.addListener('changeMine',function(url){



self.setState({



avatar:url



})



});



//通知开始,获取到url,调用setState 方法,刷新状态机,这时候实时的刷新了‘我的’图标



//最后别忘了移除通知



componentWillUnmount(){



this.listener.remove();



}

js 向 js 发送数据

DeviceEventEmitter.emit('自定义名称',发送数据);

例:边看边买退出登录之后,我的淘宝和详情页的钱包数据应该改变。这时,我们可以在退出登录请求返回退出登录成功时发送一个通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
userInfo.userLogout((success) = {



if (success) {




DeviceEventEmitter.emit('taobaoBind',{taobaoBind:false,walletSum:0.00,couponNum:0});



const nav = this.props.navigator;



const routers = nav.getCurrentRoutes();



if (routers.length 1) {



nav.pop();



}



}



});

然后在我的淘宝和详情页接收通知,并使用setState改变数据

DeviceEventEmitter.addListener('taobaoBind',(events) ={this.setState({walletSum : events.walletSum});});

js接受数据

DeviceEventEmitter.addListener('名称',(events) ={使用数据events});

android向js发送数据

1
2
3
4
5
6
7
8
WritableMap params = Arguments.createMap();

params.putString("message",msg.obj.toString());


reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)

.emit(eventName, params);

例:扫码轮询时,扫码成功可以向扫码页发送一个扫码成功的状态,输入密码完成时,也可以发送一个状态,使扫码页自动关闭。并将用户信息发给我的淘宝,详情页等。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×