react-native 性能优化

什么样的app才是一个优秀的app呢?
  • 安装包的体积小
  • 启动速度快
  • 使用流畅、不卡顿
  • 用户交互友好
  • 报错或者闪退次数少

一、安装包大小

1、第三方库部分引用

使用第三方库会增加安装包的大小,有一些第三方库写得很完美(代码很多),但是我们可能只需使用库里面很小的一部分代码。这种情况下,我们可以把相应的代码拷贝到项目里面,而不是直接引用整个库。比如:助手里面使用了SDWebImage第三方库,里面支持很多图片格式,但是项目中只是为了支持gif图片,因此我们只需要导入与gif相关的文件。

2、资源文件压缩

项目中一般都会有大量的图片资源文件,如果没有进行压缩,会增加app的体积。使用TinyPNG去进行压缩,一般可以减少50%-70%的大小。手动使用tiny去压缩,不但会增加开发时间,而且容易遗忘,因此建议写成脚本,在项目执行打包的时候去统一处理。

3、定期清除没有使用的资源文件、代码

版本迭代会造成一些资源文件或者代码被弃用,建议定期检查项目,删掉一些没用到的资源文件(脚本?)和代码。

二、首屏渲染优化

1、拆包

在 react-native 执行 JS 代码之前,必须将代码加载到内存中并进行解析。如果你加载了一个 50MB 的 bundle,那么所有的 50mb 都必须被加载和解析才能被执行,这会导致首屏渲染时间变得很长。使用拆包技术将bundle拆成不同的业务包, 启动时根据需要加载相应的模块,之后再逐渐按需加载更多的包。

(1)moles-packer:由携程框架团队研发的,与携程moles框架配套使用的React Native 打包和拆包工具,同时支持原生的 React Native 项目。重写了react native自带的打包工具,适合RN0.4.0版本之前的分包。维护少,现在基本没有多少人使用,兼容性差。

(2)diff patch:由谷歌团队研发,兼容多种语言。原理是旧包与新包进行差异比对,生成补丁文件,压缩上传到服务器。app请求接口把补丁文件下载到本地,与旧包合成新包。
优点:打包生成的补丁文件体积小;
缺点:跨版本升级的时候不容易维护
git: https://github.com/google/diff-match-patch

(3)metro bundle:facebook官方提供的,支持RN 0.50及以上版本,并随着RN版本的迭代不断完善。文档链接:https://facebook.github.io/metro/docs/en/getting-started

2、内联引用

内联引用(require 代替 import)可以延迟模块或文件的加载,直到实际需要该文件。

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}>
          <Text>Load</Text>
        </TouchableOpacity>
        {this.state.needsExpensive ? <VeryExpensive /> : null}
      </View>
    );
  }
}

3、bundle预加载

调用require会造成额外的开销。因为当遇到尚未加载的模块时,require需要通过bridge来发送消息。这主要会影响到启动速度,因为在应用程序加载初始模块时可能触发相当大量的请求调用。幸运的是,我们可以配置一部分模块进行预加载。具体的步骤可以参考官方文档:https://reactnative.cn/docs/performance/

三、app的流畅度

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

1、减少不必要的渲染

在一个复杂应用的根组件上调用了this.setState,从而导致一次开销很大的子组件树的重绘,可想而知,这可能会花费 200ms 也就是整整 12 帧的丢失。此时,任何由 JavaScript 控制的动画都会卡住。只要卡顿超过 100ms,用户就会明显的感觉到。

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

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

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

3、Touchable 系列组件的优化

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

handleOnPress() {
  this.requestAnimationFrame(() => {
    this.doExpensiveAction();
  });
}
4、动画优化

React Native 提供了两个互补的动画系统:用于创建精细的交互控制的动画Animated和用于全局的布局动画LayoutAnimation

Animated的接口一般会在 JavaScript 线程中计算出所需要的每一个关键帧,而LayoutAnimation则利用了Core Animation,使动画不会被 JS 线程和主线程的掉帧所影响。但是,LayoutAnimation只工作在“一次性”的动画上("静态"动画) -- 如果动画可能会被中途取消,你还是需要使用Animated

5、耗时的操作等动画结束后再进行

Interactionmanager可以将一些耗时较长的工作安排到所有互动或动画完成之后再进行。这样可以保证JavaScript动画的流畅运行。

InteractionManager.runAfterInteractions(() => {
  // ...耗时较长的同步的任务...
});
6、ListView 初始化渲染太慢以及列表过长时滚动性能太差

用新的FlatList或者SectionList组件替代。除了简化了API,这些新的列表组件在性能方面都有了极大的提升, 其中最主要的一个是无论列表有多少行,它的内存使用都是常数级的。

如果你的FlatList渲染得很慢, 请确保你使用了getItemLayout,它通过跳过对items的处理来优化你的渲染速度。除此之后,你还可以使用initialNumToRender设置首屏渲染数量。

四、用户体验优化

1、TextInput
  • 自动聚焦第一个字段
  • 使用占位符文本作为预期数据格式的示例
  • 文本格式化(电话、身份证、银行卡)
  • 选择键盘类型(例如电子邮件,数字)
  • 确保返回按钮聚焦下一个字段或提交表单
  • 点击空白处收起键盘
  • 编辑时显示清除按钮
2、键盘可见时管理布局

软件键盘几乎占据了屏幕的一半。如果您有可以被键盘覆盖的交互式元素,请确保使用该KeyboardAvoidingView组件仍可访问它们。

3、扩大响应区域

在手机上按按钮时很难非常精确。确保所有交互式元素都是44x44或更大。一种方法是为元素留出足够的空间paddingminWidth并且minHeight样式值对此有用。或者,您可以使用[hitSlop]属性来增加交互区域而不影响布局。

4、使用Android Ripple

Android API 21+使用材质设计纹波,在用户触摸屏幕上的可交互区域时为其提供反馈。React Native通过TouchableNativeFeedback组件公开它。使用这种可触摸效果而不是不透明度或高光效果通常会让您的应用感觉更适合平台。也就是说,使用它时需要小心,因为它不适用于iOS或Android API <21,因此您需要回退使用iOS上的其他可触摸组件之一。您可以使用像react-native-platform-touchable这样的库来为您处理平台差异。

五、错误上报

1、官方提供了方法:global.ErrorUtils.setGlobalHandler来监听全局的错误,可以参考以下代码:
global.ErrorUtils.setGlobalHandler((e) => {
      if (!e || !(e instanceof Error) || !e.stack) return {}
      try {
        const stack = e.stack.toString().split(/\r\n|\n/), frameRE = /:(\d+:\d+)[^\d]*$/
        while (stack.length) {
          const frame = frameRE.exec(stack.shift())
          if (frame) {
            // 堆栈信息里面的行数和列数
            const position = frame[1].split(':')
            return { line: position[0], column: position[1] }
          }
        }
      } catch ( e ) {
        return {}
      }
    })
 
2、使用SourceMap查看

我们可以通过完整的srouceMap和出错的line、column来准确还原发生错误时代码上下文。在RN当中我们可以在执行打包命令的时候带上--sourcemap-output youbundle.map来生成。

const { SourceMapConsumer, SourceMapGenerator, SourceNode } = require('source-map')
const fs = require('fs')

/**
 * rawMap
 */
const rawMap = JSON.parse(fs.readFileSync('./index.map').toString())

const smc = new SourceMapConsumer(rawMap)
const position = smc.originalPositionFor({
    line: 1,
    column: 75422
  })

const { source, line, column } = position

// output
console.log(`事故发生现场:${source},位于第${line}行,第${column}列!`)
3、第三方库:Sentry
  • 提供了完善的客户端SDK,Android、iOS、react-native、web全平台支持。各端只需配置上报地址,其余SDK全搞定;
  • 支持上传sourcemap进行源码定位。由于RN运行在客户端中,与普通运行在浏览器的js相比错误堆栈有自己的特点。Sentry是目前调研中唯一一个默认支持react-native错误分析的平台。
  • Sentry从SDK到后端服务都开源,可以私有化部署

文档:https://docs.sentry.io/clients/react-native/
github:https://github.com/getsentry/sentry-react-native

参考链接

  1. 性能:https://reactnative.cn/docs/performance/
  2. 体验优化:https://reactnative.cn/docs/improvingux/
  3. 错误上报: https://hasaki.xyz/blog/2017-09-12-rn%E6%80%A7%E8%83%BD%E6%A3%80%E6%B5%8B%E5%92%8C%E9%94%99%E8%AF%AF%E6%8D%95%E6%8D%89/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容