什么样的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或更大。一种方法是为元素留出足够的空间padding
,minWidth
并且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