React Native 性能优化指南【全网最全,值得收藏】

2020 年谈 React Native,在突飞猛进的前端圈,可能算比较另类了。文章动笔以前我也犹豫过,可是想到写技术文章又不是赶时髦,啥新潮写啥,因此仍是动笔写了这篇 React Native 性能优化的文章。javascript

本文谈到的 React Native 性能优化,还没到修改 React Native 源码那种地步,因此通用性很强,对大部分 RN 开发者来讲都用得着。html

本文的内容,一部分是 React/RN/Android/iOS 官方推荐的优化建议,一部分是啃源码发现的优化点,还有一部分是能够解决一些性能瓶颈的优秀的开源框架。本文总结的内容你不多在网络上看到,因此看完后必定会有所收获。若是以为写的不错,请不要吝啬你的赞,把这篇 1w 多字的文章分享出去,让更多的人看到。前端

看文章前要明确一点,一些优化建议并非对全部团队都适用。有的团队把 React Native 当加强版网页使用,有的团队用 React Native 实现非核心功能,有的团队把 React Native 当核心架构,不一样的定位须要不一样的选型。对于这些场景,我在文中也会提一下,具体使用还须要各位开发者定夺。java


目录:

  • 1、减小 re-render
  • 2、减轻渲染压力
  • 3、图片优化那些事
  • 4、对象建立调用分离
  • 5、动画性能优化
  • 6、长列表性能优化
  • 7、React Native 性能优化用到的工具
  • 8、推荐阅读

1、减小 re-render

由于 React Native 也是 React 生态系统的一份子,因此不少 React 的优化技巧能够用到这里,因此文章刚开始先从你们最熟悉的地方开始。react

对于 React 来讲,减小 re-render 能够说是收益最高的事情了。android

1️⃣ shouldComponentUpdate

📄 文档react.docschina.org/docs/optimi…webpack

简单式例:git

class Button extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    return false;
  }

  render() {
    return <button color={this.props.color} />; } } 复制代码

不管哪篇文章,谈到 React 性能优化,shouldComponentUpdate 必定是座上宾。github

咱们经过这个 API,能够拿到先后状态的 state/props,而后手动检查状态是否发生了变动,再根据变动状况来决定组件是否须要从新渲染。web

🔗 官方文档shouldComponentUpdate 的做用原理和使用场景已经说的很是清晰了,我就没有必要搬运文章了。在实际项目中,阅文集团的 🔗 React Native 应用「元气阅读」也作了很好的示范,🔗 Twitter 的性能优化分享也作的图文并茂,可有很高的参考价值,对此感兴趣的同窗能够点击跳转查看。

在此我想提醒的是,shouldComponentUpdate 是强业务逻辑相关的。 若是使用这个 API,你必须考虑和此组件相关的全部 props 和 state,若是有遗漏,就有可能出现数据和视图不统一的状况。因此使用的时候必定很是当心。

2️⃣️ React.memo

📄 文档react.docschina.org/docs/react-…

React.memo 是 React v16.6 中引入的新功能,是一个专门针对 React 函数组件的高阶组件。

默认状况下,它和 PureComponent 同样,都是进行浅比较,由于就是个高阶组件,在原有的组件上套一层就能够了:

const MemoButton = React.memo(function Button(props) {
  return <button color={this.props.color} />; }); 复制代码

若是想和 shouldComponentUpdate 同样,自定义比较过程,React.memo 还支持传入自定义比较函数:

function Button(props) {
  return <button color={this.props.color} />; } function areEqual(prevProps, nextProps) { if (prevProps.color !== nextProps.color) { return false; } return true; } export default React.memo(MyComponent, areEqual); 复制代码

值得注意的是areEqual() 这个函数的返回值和 shouldComponentUpdate 正好相反,若是 props 相等,areEqual()返回的是 trueshouldComponentUpdate 却返回的是 false

3️⃣ React.PureComponent

📄 文档react.docschina.org/docs/react-…

简单式例:

class PureComponentButton extends React.PureComponent {
  render() {
    return <button color={this.props.color} />; } } 复制代码

shouldComponentUpdate 相对应,React 还有一个相似的组件 React.PureComponent,在组件更新前对 props 和 state 作一次浅比较。因此涉及数据嵌套层级过多时,好比说你 props 传入了一个两层嵌套的 Object,这时候 shouldComponentUpdate 就很为难了:我究竟是更新呢仍是不更新呢?

考虑到上面的状况,我在项目中通常不多用 PureComponent虽然很简单易用,可是面对复杂逻辑时,反而不如利用 shouldComponentUpdate 手动管理简单粗暴。固然这个只是我的的开发习惯,社区上也有其余的解决方案:

  • 把组件细分为很小的子组件,而后统一用 PureComponent 进行渲染时机的管理
  • 使用 immutable 对象,再配合 PureComponent 进行数据比较(🔗 参考连接:有赞 React 优化
  • ......

在这个问题上仁者见仁智者见智,在不影响功能的前提下,主要是看团队选型,只要提早约定好,其实在平常开发中工做量都是差很少的(毕竟不是每一个页面都有必要进行性能优化)。


2、减轻渲染压力

React Native 的布局系统底层依赖的是 🔗 Yoga 这个跨平台布局库,将虚拟 DOM 映射到原生布局节点的。在 Web 开发中,99% 的状况下都是一个 Virtual DOM 对应一个真实 DOM 的,那么在 React Native 中也是一一对应的关系吗?咱们写个简单的例子来探索一下。

咱们先用 JSX 写两个橙色底的卡片,除了卡片文字,第一个卡片还嵌套一个黄色 View,第二个卡片嵌套一个空 View:

// 如下示例 code 只保留了核心结构和样式,领会精神便可
render() {
  return (
    <View> <View style={{backgroundColor: 'orange'}}> <View style={{backgroundColor: 'yellow'}}> <Text>Card2</Text> </View> </View> <View style={{backgroundColor: 'orange'}}> <View> <Text>Card2</Text> </View> </View> </View>
  );
};
复制代码

react-devtools 查看 React 嵌套层级时以下所示:

从上图中能够看出,React 组件和代码写的结构仍是一一对应的。

咱们再看看 React Native 渲染到原生视图后的嵌套层级(iOS 用 Debug View Hierarchay,Android 用 Layout Inspector):

从上图能够看出,iOS 是一个 React 节点对应一个原生 View 节点的;Android 第二个卡片的空白 View 却不见了!

若是咱们翻一翻 React Native 的源码,就会发现 React Native Android UI 布局前,会对只有布局属性的 View(LAYOUT_ONLY_PROPS 源码)进行过滤,这样能够减小 View 节点和嵌套,对碎片化的 Android 更加友好。

经过这个小小的例子咱们能够看出,React 组件映射到原生 View 时,并非一一对应的,咱们了解了这些知识后,能够如何优化布局呢?

1️⃣ 使用 React.Fragment 避免多层嵌套

📄 React Fragments 文档zh-hans.reactjs.org/docs/fragme…

咱们先从最熟悉的地方讲起——React.Fragment。这个 API 可让一个 React 组件返回多个节点,使用起来很简单:

render() {
  return (
    <React.Fragment> <ChildA /> <ChildB /> <ChildC /> </React.Fragment> ); } // 或者使用 Fragment 短语法 render() { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ); } 复制代码

Fragments 做用仍是蛮明显的:避免你多写一层 View。用处仍是很广的,好比说本身业务上封装的 React 组件,React Native 官方封装的组件(好比说 ScrollView or Touchable* 组件 ),活用这个属性,能够减小你的 View 嵌套层级。

2️⃣ 减小 GPU 过分绘制

咱们在业务开发时,常常会遇到这种场景:整个界面的背景色是白色的,上面又加了一个白色背景的卡片组件,卡片内部又包含了一个白色背景的小组件......

// 如下示例 code 只保留了核心结构和样式,领会精神便可
render() {
  return (
    <View> <View style={{backgroundColor: 'white'}}> <View style={{backgroundColor: 'white'}}> <Text style={{backgroundColor: 'white'}}>Card1</Text> </View> </View> <View> <View> <Text>Card2</Text> </View> </View> </View>
  );
};
复制代码

首先咱们要明确一点,屏幕上的每一个像素点的颜色,是由多个图层的颜色决定的,GPU 会渲染这些图层混合后的最终颜色,可是,iOS 和 Android 的 GPU 渲染机制是不一致的。

虽然上面的代码最后的的渲染结果在显示上都是白色的,可是 GPU 的优化是不同的。咱们用 iOS 的 Color Blended Layers 和 Android 的🔗 GPU 过分绘制调试工具查看最后的渲染结果:

对于 iOS 来讲,出现红色区域,就说明出现了颜色混合:

  • Card1 的几个 View 都设置了非透明背景色,GPU 获取到顶层的颜色后,就再也不计算下层的颜色了
  • Card2 的 Text View 背景色是透明的,因此 GPU 还要获取下一层的颜色进行混合

对于 Android 来讲,GPU 会画蛇添足地渲染对用户不可见的像素。有一个颜色指示条:白 -> 蓝 -> 绿 -> 粉 -> 红,颜色越日后表示过分绘制越严重。

  • Card1 的几个 View 都设置了非透明背景色,红色表示起码发生了 4 次过分绘制
  • Card2 只有文字发生了过分绘制

在过渡绘制这个测试上,iOS 和 Android 的实验结果几乎是彻底相反的,因此解决方案确定不是一箭双鵰的,我我的认为,React Native 开发作视图优化时,应该优先优化 Android,因此咱们能够从如下几点优化:

  • 减小背景色的重复设置:每一个 View 都设置背景色的话,在 Android 上会形成很是严重的过分绘制;而且只有布局属性时,React Native 还会减小 Android 的布局嵌套
  • 避免设置半透明颜色:半透明色区域 iOS Android 都会引发过分绘制
  • 避免设置圆角:圆角部位 iOS Android 都会引发过分绘制
  • 避免设置阴影:阴影区域 iOS Android 都会引发过分绘制
  • ......

避免 GPU 过分绘制的细节太多了,通常页面不须要这种精细化管理,长列表优化时能够考虑一下这个方向。


3、图片优化那些事

性能优化的另外一个大头就是图片。这里的图片优化不只仅指减小图片大小,减小 HTTP 带宽占用,我会更多的讨论一些 Image 组件上的优化,好比说缓存控制,图片采样等技术。

️1️⃣️ Image 组件的优化项

React Native 的 Image 图片组件,若是只是做为普通的图片展现组件,那它该有的都有了,好比说:

  • 加载本地/网络图片
  • 自动匹配 @2x/@3x 图片
  • 图片加载事件:onLoadStart/onLoad/onLoadEnd/onError
  • loading 默认图 or loading 指示器
  • ......

可是,若是你要把它当一个图片下载管理库用时,就会很是的难受,由于 Image 的这几个属性在 iOS/Android 上有不一样的表现,有的实现了有的没有实现,用起来很是不顺手。

在讲解图片优化前,咱们先想一下,一个基本的图片下载管理库要实现什么:

  1. 图片类型:首先你的主要职责是加载图片,你起码能加载多种图片类型
  2. 下载管理:在加载多张图片的场景,能管理好多个请求,能够控制图片加载的优先级
  3. 缓存管理:作好三级缓存,不能每一个图片都要请求网络,均衡好内存缓存和磁盘缓存的策略
  4. 多图加载:大量图片同时渲染时,如何让图片迅速加载,减小卡顿

针对上面的 4 条原则,咱们来一一刨析 Image 组件。

1.图片类型

基础的 png/jpg/base64/gif 格式,支持良好。不过要注意的是,想要 Android 加载的 gif 图片动起来,要在 build.gradle 里面加一些依赖,具体内容能够看这个 🔗 连接

若是要加载 webp 格式的图片,就有些问题了。做为 Google 推出的一种图片格式,Android 天然是支持的,可是 iOS 就不支持了,须要咱们安装一些第三方插件。

2.下载管理

先说结论,Image 组件对图片的下载管理能力基本为 0。

Image基本上只能监听单张图片的加载流程:onLoadStart/onLoad/onLoadEnd/onError,若是要控制多张图片的下载优先级,对不起,没有。

3.缓存管理

缓存这里要从两方面说,一是经过 HTTP 头信息管理缓存,二是直接经过一些组件属性管理缓存。

Image 组件请求网络图片时,实际上是能够加 HTTP header 头信息的,这样就能够利用 HTTP 缓存来管理图片,写法以下面代码所示:

<Image
  source={{
    uri: 'https://facebook.github.io/react/logo-og.png',
    method: 'POST',
    headers: {
      Pragma: 'no-cache',
    },
    body: 'Your Body goes here',
  }}
  style={{width: 400, height: 400}}
/>
复制代码

具体的控制参数能够参考 🔗 MDN HTTP 缓存,这里就不细说了。

直接经过属性控制图片缓存,iOS 有。Android?对不起,没有。

iOS 能够经过 source 参数里的 cache 字段控制缓存,属性也是很是常见的那几种:默认/不使用缓存/强缓存/只使用缓存。具体的使用能够看 🔗 iOS Image 缓存文档

4.多图加载

都快到 5G 时代了,短视频/VLog 你们都每天刷了,更不用说多图场景了,基本上已是互联网应用的标配了。

讲图片加载前先明确一个概念:图片文件大小 != 图片加载到内存后的大小

咱们常说的 jpg png webp,都是原图压缩后的文件,利于磁盘存储和网络传播,可是在屏幕上展现出来时,就要恢复为原始尺寸了。

React Native 性能优化——图片内存优化

好比说一张 1024x768 的 png 图片,可能磁盘空间就十几 kb,不考虑分辨率等问题,加载到内存里,就要占用 3 Mb。

// 不一样的分辨率/文件夹/编码格式,都会带来数值差别
// 下面的计算只是最通常的场景,领会精神便可

(1024 * 768 * 4 * 8) / (8 * 1024 * 1024) = 3 MB
(长 * 宽 * 每一个像素占用字节数) / (8 * 1024 * 1024) = 3 MB
复制代码

上面只是 1024x768 的图片,若是图片尺寸增长一倍,图片在内存里的大小是按平方倍数增加的,数量一多后,内存占用仍是很恐怖的。

在多图加载的场景里,通过实践,iOS 无论怎么折腾,表现都比较好,可是 Android 就容易出幺蛾子。下面咱们就详细说说 Android 端如何优化图片。

在一些场景里,Android 会内存爆涨,帧率直接降为个位数。这种场景每每是小尺寸 Image 容器加载了特别大的图片,好比说 100x100 的容器加载 1000x1000 的图片,内存爆炸的缘由就是上面说的缘由。

那么这种问题怎么解决呢?Image 有个 resizeMethod 属性,就是解决 Android 图片内存暴涨的问题。当图片实际尺寸和容器样式尺寸不一致时,决定以怎样的策略来调整图片的尺寸。

  • resize小容器加载大图的场景就应该用这个属性。原理是在图片解码以前,会用算法对其在内存中的数据进行修改,通常图片大小大概会缩减为原图的 1/8。
  • scale:不改变图片字节大小,经过缩放来修改图片宽高。由于有硬件加速,因此加载速度会更快一些。
  • auto:文档上说是经过启发式算法自动切换 resize 和 scale 属性。这个启发式算法很是误导人,第一眼看上去还觉得是会对比容器尺寸和图片尺寸采用不一样策略。但我看了一下源码,它只是单纯的判断图片路径,若是是本地图片,就会用 resize,其余都是 scale 属性,因此 http 图片都是 scale 的,咱们还得根据具体场景手动控制。

顺便提一下,Android 图片加载的时候,还会有一个 easy-in 的 300ms 加载动画效果,看上去会以为图片加载变慢了,咱们能够经过设置 fadeDuration 属性为 0,来关闭这个加载动画。

2️⃣ 优先使用 32 位色彩深度的图片

📄 色彩深度 wikigithub.com/DylanVann/r…

色彩深度这个概念其实前面也提了一下,好比说咱们经常使用的带透明度 PNG 图片,就是 32 位的:

  • R:红色,占据 8 bit
  • G:绿色,占据 8 bit
  • B:蓝色,占据 8 bit
  • A:透明通道,占据 8 bit

为啥推荐使用 32 bit 图片呢?直接缘由有 2 个:

  1. Android 推荐使用 🔗 ARGB_8888 格式的图片,由于这种图片显示效果更好
  2. iOS GPU 只支持加载 32 bit 的图片。若是是其余格式的(好比说 24 bit 的 jpg),会先在 CPU 里转为 32 bit,再传给 GPU

虽然推荐 32 bit 图片,可是说实话,这个对前端开发是不可控的,由于图片来源通常就 2 个:

  1. 设计师的切图,由设计师控制
  2. 网络上的图片,由上传者控制

因此想针对这一点进行优化的话,沟通成本挺高,收益反而不高(通常只在长列表有些问题),但也是图片优化的一个思路,故放在这一节里。

3️⃣ Image 和 ImageView 长宽保持一致

前面举了一个 100x100 的 ImageView 加载 1000x1000 Image 致使 Android 内存 OOM 的问题,咱们提出了设置 resizeMethod={'resize'} 的方法来缩减图片在内存中的体积。其实这是一种无奈之举,若是能够控制加载图片的大小,咱们应该保持 Image 和 ImageView 长宽一致。

首先咱们看看长宽不一致会引发的问题:

  • Image 小于 ImageView:图片不清晰,表情包电子包浆质感
  • Image 大于 ImageView:浪费内存,有可能会引发 OOM
  • 尺寸不一致会带来抗锯齿计算,增长了图形处理负担

React Native 开发时,布局使用的单位是 pt,和 px 存在一个倍数关系。在加载网络图片时,咱们可使用 React Native 的 🔗 PixelRatio.getPixelSizeForLayoutSize 方法,根据不一样的分辨率加载不一样尺寸的图片,保证 Image 和 ImageView 长宽一致。

4️⃣ 使用 react-native-fast-image

📄 react-native-fast-image 文档github.com/DylanVann/r…

通过上面的几个 Image 属性分析,综合来看,Image 组件对图片的管理能力仍是比较弱的,社区上有个 Image 组件的替代品:react-native-fast-image

它的底层用的是 🔗 iOS 的 SDWebImage 🔗 Android 的 Glide 。这两个明星图片下载管理库,原生开发同窗确定很熟悉,在缓存管理,加载优先级和内存优化上都有不错的表现。并且这些属性都是双平台可用,这个库都封装好了,可是官网上只有基础功能的安装和配置,若是想引入一些功能(好比说支持 WebP),仍是须要查看 SDWebImage 和 Glide 的文档的。

引入前我仍是想提醒一下,React Native 的 Android Image 组件底层封装了 FaceBook 的 Fresco,引入这个库至关于又引入了 Glide,包体积不可避免的会变大,因此引入以前可能还要均衡一下。

5️⃣ 图片服务器辅助

前面说的都是从 React Native 侧优化图片,可是一个产品历来不是单打独斗,借助服务端的力量其实能够省不少事。

1.使用 WebP

WebP 的优点不用我多说,一样的视觉效果,图片体积会明显减小。并且能够显著减少 CodePush 热更新包的体积(热更新包里,图片占用 90% 以上的体积)。

虽然 WebP 在前端解压耗时可能会多一点点,可是考虑到传输体积缩小会缩短网络下载时间,总体的收益仍是不错的。

2.图床定制图片

通常比较大的企业都有内建图床和 CDN 服务,会提供一些自定制图片的功能,好比说指定图片宽高,控制图片质量。固然一些比较优秀的第三方对象存储也提供这些功能,好比说🔗 七牛云 图片处理

借用云端图片定制功能,前端能够轻松经过控制 URL 参数控制图片属性

好比说 Android 经过 resizeMethodresize 更改图片字节大小,虽然也能够解决问题,可是这个算法仍是在前端运行的,仍是会占用用户内存资源。咱们把连接改为:

https://www.imagescloud.com/image.jpg/0/w/100/h/100/q/80
// w: 长为 100 px
// h: 宽最多为 100 px
// q: 压缩质量为 80
复制代码

这样子就能够把计算转移到服务端,减小前端的 CPU 占用,优化前端总体的性能。


4、对象建立调用分离

对象建立和调用分离,其实更多的是一种编码习惯。

咱们知道在 JavaScript 里,啥都是对象,而在 JS 引擎里,建立一个对象的时间差很少是调用一个已存在对象的 10 多倍。在绝大部分状况下,这点儿性能消耗和时间消耗根本不值一提。但在这里仍是要总结一下,由于这个思惟习惯仍是很重要的。

1️⃣ public class fields 语法绑定回调函数

📄 文档zh-hans.reactjs.org/docs/handli…

做为一个前端应用,除了渲染界面,另外一个重要的事情就是处理用户交互,监听各类事件。因此在组件上绑定各类处理事件也是一个优化点。

在 React 上如何处理事件已是个很是经典的话题了,我搜索了一下,从 React 刚出来时就有这种文章了,动不动就是四五种处理方案,再加上新出的 Hooks,又能玩出更多花样了。

最多见的绑定方式应该是直接经过箭头函数处理事件:

class Button extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    return <button onClick={(e) => this.handleClick(e)}>Click me</button>;
  }
}
复制代码

但这种语法的问题是每次 Button 组件从新渲染时,都会建立一个 handleClick() 函数,当 re-render 的次数比较多时,会对 JS 引擎形成必定的垃圾回收压力,会引发必定的性能问题。

🔗 官方文档里比较推荐开发者使用 🔗 public class fields 语法 来处理回调函数,这样的话一个函数只会建立一次,组件 re-render 时不会再次建立:

class Button extends React.Component {
  // 此语法确保 handleClick 内的 this 已被绑定。
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}
复制代码

在实际开发中,通过一些数据对比,因绑定事件方式的不一样引发的性能消耗基本上是能够忽略不计的,re-render 次数过多才是性能杀手。但我认为这个意识仍是有的,毕竟从逻辑上来说,re-render 一次就要建立一个新的函数是真的不必。

2️⃣️ public class fields 语法绑定渲染函数

这个其实和第一个差很少,只不过把事件回调函数改为渲染函数,在 React Native 的 Flatlist 中很常见。

不少新人使用 Flatlist 时,会直接向 renderItem 传入匿名函数,这样每次调用 render 函数时都会建立新的匿名函数:

render(){
  <FlatList
    data={items}
    renderItem={({ item }) => <Text>{item.title}</Text>}
  />
}
复制代码

改为 public class fields 式的函数时,就能够避免这个现象了:

renderItem = ({ item }) => <Text>{item.title}</Text>;

render(){
  <FlatList
    data={items}
    renderItem={renderItem}
  />
}
复制代码

一样的道理,ListHeaderComponentListFooterComponent 也应该用这样写法,预先传入已经渲染好的 Element,避免 re-render 时从新生成渲染函数,形成组件内部图片从新加载出现的闪烁现象。

3️⃣️ StyleSheet.create 替代 StyleSheet.flatten

📄 文档reactnative.cn/docs/styles…

StyleSheet.create 这个函数,会把传入的 Object 转为优化后的 StyleID,在内存占用和 Bridge 通讯上会有些优化。

const styles = StyleSheet.create({
  item: {
    color: 'white',
  },
});

console.log(styles.item) // 打印出的是一个整数 ID
复制代码

在业务开发时,咱们常常会抽出一些公用 UI 组件,而后传入不一样的参数,让 UI 组件展现不同的样式。

为了 UI 样式的灵活性,咱们通常会使用 StyleSheet.flatten,把经过 props 传入自定义样式和默认样式合并为一个样式对象:

const styles = StyleSheet.create({
  item: {
    color: 'white',
  },
});

StyleSheet.flatten([styles.item, props.style]) // <= 合并默认样式和自定义样式
复制代码

这样作的好处就是能够灵活的控制样式,问题就是使用这个方法时,会🔗 递归遍历已经转换为 StyleID 的样式对象,而后生成一个新的样式对象。这样就会破坏 StyleSheet.create 以前的优化,可能会引发必定的性能负担。

固然本节不是说不能用 StyleSheet.flatten通用性和高性能不能同时兼得,根据不一样的业务场景采起不一样的方案才是正解。

4️⃣ 避免在 render 函数里建立新数组/对象

咱们写代码时,为了不传入 [] 的地方因数据没拿到传入 undefined,常常会默认传入一个空数组:

render() {
  return <ListComponent listData={this.props.list || []}/> } 复制代码

其实更好的作法是下面这样的:

const EMPTY_ARRAY = [];

render() {
    return <ListComponent listData={this.props.list || EMPTY_ARRAY}/> } 复制代码

这个其实算不上啥性能优化,仍是前面再三强调的思路:对象建立和调用分离。毕竟每次渲染的时候从新建立一个空的数组/对象,能带来多大的性能问题?

[] 改成统一的 EMPTY_ARRAY 常量,其实和平常编码中避免出现 Magic Number 同样,算一种编程习惯,但我以为这种优化能够归到这个类别里,因此专门提一下。


5、动画性能优化

动画流畅很简单,在大部分的设备上,只要保证 60fps 的帧率就能够了。但要达到这个目标,在 React Native 上仍是有些问题的,我画了一张图,描述了目前 React Native 的基础架构(0.61 版本)。

  • UI Thread:在 iOS/Android 上专门绘制 UI 的线程
  • JS Thread:咱们写的业务代码基本都在这个线程上,React 重绘,处理 HTTP 请求的结果,磁盘数据 IO 等等
  • other Thread:泛指其余线程,好比说数据请求线程,磁盘 IO 线程等等

上图咱们能够很容易的看出,JS 线程太忙了,要作的事情太多了。并且 UI Thread 和 JS Thread 以前通讯是异步的(Async Bridge),只要其它任务一多,就很难保证每一帧都是及时渲染的。

分析清楚了,React Native 动画优化的方向天然而然就出来了:

  • 减小 JS Thread 和 UI Thread 之间的异步通讯
  • 尽可能减小 JS Thread 侧的计算

1️⃣ 开启 useNativeDrive: true

📄 文档facebook.github.io/react-nativ…

JS Thread 和 UI Thread 之间是经过 JSON 字符串传递消息的。对于一些可预测的动画,好比说点击一个点赞按钮,就跳出一个点赞动画,这种行为彻底能够预测的动画,咱们可使用 useNativeDrive: true 开启原生动画驱动。

经过启用原生驱动,咱们在启动动画前就把其全部配置信息都发送到原生端,利用原生代码在 UI 线程执行动画,而不用每一帧都在两端间来回沟通。如此一来,动画一开始就彻底脱离了 JS 线程,所以此时即使 JS 线程被卡住,也不会影响到动画了。

使用也很简单,只要在动画开始前在动画配置中加入 useNativeDrive: true 就能够了:

Animated.timing(this.state.animatedValue, {
  toValue: 1,
  duration: 500,
  useNativeDriver: true // <-- 加上这一行
}).start();
复制代码

开启后全部的动画都会在 Native 线程运行,动画就会变的很是丝滑顺畅。

通过各类暴力测试,使用原生驱动动画时,基本没有掉帧现象,可是用 JS 驱动动画,一旦操做速度加快,就会有掉帧现象。

值得注意的是,useNativeDriver 这个属性也有着局限性,只能使用到只有非布局相关的动画属性上,例如 transformopacity。布局相关的属性,好比说 height 和 position 相关的属性,开启后会报错。并且前面也说了,useNativeDriver 只能用在可预测的动画上,好比说跟随手势这种动画,useNativeDriver 就用不了的。

2️⃣ 使用 setNativeProps

📄 文档facebook.github.io/react-nativ…

setNativeProps 这个属性,至关于直接操做浏览器的 DOM。React 官方通常是不推荐直接操做 DOM 的,但业务场景变幻无穷,总会遇到一些场景不得不操做 DOM,在React Native 里也是一样的道理。

好比说下面的动图,在屏幕中上下滚动时,y 轴上的偏移能够经过 ScrollView#onScroll 属性开启 useNativeDrive: true 来优化滚动体验。可是咱们能够看到,随着上下滑动,圆圈里的数字也是随之变化的。

若是把数字存在 this.state 里, 每次滑动不可避免的要进行大量的 setState,React 端会进行大量的重绘操做,可能会引发掉帧。咱们这里就能够用 setNativeProps,避免 React 端重绘,至关于直接修改 DOM 上的数字,这样可让动画更加流畅。

3️⃣ 使用 InteractionManager

📄 文档facebook.github.io/react-nativ…

原生应用感受如此流畅的一个重要缘由就是在互动和动画的过程当中避免繁重的操做。

在 React Native 里,JS 线程太忙了,啥都要干,咱们能够把一些繁重的任务放在 InteractionManager.runAfterInteractions() 里,确保在执行前全部的交互和动画都已经处理完毕。

InteractionManager.runAfterInteractions(() => {
  // ...须要长时间同步执行的任务...
});
复制代码

在 React Native 官方提供的组件里,PanResponder、Animated,VirtualizedList 都用了 InteractionManager,为的就是平衡复杂任务和交互动画之间的执行时机。

4️⃣ 使用 react-native-reanimated 和 react-native-gesture-handler

📺 视频教程www.youtube.com/channel/UC8…

📄 react-native-gesture-handler 文档github.com/software-ma…

📄 react-native-reanimated 文档github.com/software-ma…

这两个库是被 Youtube 一个自由软件开发者博主 🔗 William Candillon 安利的,后面查了一下,也是 Expo 默认内置动画库和手势库。

这两个库目的就是替代 React Native 官方提供的🔗 手势库🔗 动画库,除了 API 更加友好,我认为最大的优点是:手势动画是在 UI Thread 运行的

咱们在前面也说了,useNativeDrive: true 这个属性,只能用在可预测的动画上。跟随手势的动画,是没法使用这个属性的,因此手势捕捉和动画,都是在 JS 侧动态计算的。

咱们举一个简单的例子:小球跟随手势移动

咱们先看看 React Native 官方提供的手势动画,能够看到 JS Thread 有大量的计算,计算结果再异步传输到 UI Thread,稍微有些风吹草动,就会引发掉帧。

若是使用 react-native-gesture-handler,手势捕捉和动画都是 UI Thread 进行的,脱离 JS Thread 计算和异步线程通讯,流畅度天然大大提高:

因此说,若是要用 React Native 构建复杂的手势动画,使用 react-native-gesture-handlerreact-native-reanimated,是一个不错的选择,能够大幅度提升动画的流畅度。

5️⃣ 使用 BindingX

📄 BindingX 文档alibaba.github.io/bindingx/gu…

BindingX 是阿里开源的一个框架,用来解决 weexReact Native上富交互问题,核心思路是将"交互行为"以表达式的方式描述,并提早预置到 Native,避免在行为触发时 JS 与 Native 的频繁通讯。


固然,引入上面几个第三方库会确定会带来必定的学习成本。对于复杂交互的页面,有的团队可能会采用原生组件来代替,好比说🔗 美团外卖就会用原生组件去实现精细动画和强交互模块,因此具体使用还要看团队的技术储备和 APP 场景。


6、长列表性能优化

在 React Native 开发中,最容易遇到的对性能有必定要求场景就是长列表了。在平常业务实践中,优化作好后,千条数据渲染仍是没啥问题的。

虚拟列表前端一直是个经典的话题,核心思想也很简单:只渲染当前展现和即将展现的 View,距离远的 View 用空白 View 展现,从而减小长列表的内存占用。

在 React Native 官网上,🔗 列表配置优化其实说的很好了,咱们基本上只要了解清楚几个配置项,而后灵活配置就好。可是问题就出在「了解清楚」这四个字上,本节我会结合图文,给你们讲述清楚这几个配置。

1️⃣ 各类列表间的关系

React Native 有好几个列表组件,先简单介绍一下:

  • ScrollView:会把视图里的全部 View 渲染,直接对接 Native 的滚动列表
  • VirtualizedList:虚拟列表核心文件,使用 ScrollView,长列表优化配置项主要是控制它
  • FlatList:使用 VirtualizedList,实现了一行多列的功能,大部分功能都是 VirtualizedList 提供的
  • SectionList:使用 VirtualizedList,底层使用 VirtualizedSectionList,把二维数据转为一维数据

还有一些其余依赖文件,有个🔗 博文的图总结的挺好的,我这里借用它的图一下:

咱们能够看出 VirtualizedList 才是主演,下面咱们结合一些示例代码,分析它的配置项。

2️⃣ 列表配置项

讲以前先写个小 demo。demo 很是简单,一个基于 FlatList 的奇偶行颜色不一样的列表。

export default class App extends React.Component {
  renderItem = item => {
    return (
      <Text style={{ backgroundColor: item.index % 2 === 0 ? 'green' : 'blue', }}> {'第 ' + (item.index + 1) + ' 个'} </Text>
    );
  }

  render() {
    let data = [];
    for (let i = 0; i < 1000; i++) {
        data.push({key: i});
    }

    return (
      <View style={{flex: 1}}> <FlatList data={data} renderItem={this.renderItem} initialNumToRender={3} // 首批渲染的元素数量 windowSize={3} // 渲染区域高度 removeClippedSubviews={Platform.OS === 'android'} // 是否裁剪子视图 maxToRenderPerBatch={10} // 增量渲染最大数量 updateCellsBatchingPeriod={50} // 增量渲染时间间隔 debug // 开启 debug 模式 /> </View> ); } } 复制代码

VirtualizedList 有个 debug 的配置项,开启后会在视图右侧显示虚拟列表的显示状况。

这个属性文档中没有说,是翻🔗 源码发现的,我发现开启它后用来演示讲解仍是很方便的,能够很直观的学习 initialNumToRender、windowSize、Viewport,Blank areas 等概念。

下面是开启 debug 后的 demo 截屏:

上面的图仍是很清晰的,右侧 debug 指示条的黄色部分表示内存中 Item,各个属性咱们再用文字描述一下:

1.initialNumToRender

首批应该渲染的元素数量,刚刚盖住首屏最好。并且从 debug 指示条能够看出,这批元素会一直存在于内存中。

2.Viewport

视口高度,就是用户能看到内容,通常就是设备高度。

3.windowSize

渲染区域高度,通常为 Viewport 的整数倍。这里我设置为 3,从 debug 指示条能够看出,它的高度是 Viewport 的 3 倍,上面扩展 1 个屏幕高度,下面扩展 1 个屏幕高度。在这个区域里的内容都会保存在内存里。

将 windowSize 设置为一个较小值,能有减少内存消耗并提升性能,可是快速滚动列表时,遇到未渲染的内容的概率会增大,会看到占位的白色 View。你们能够把 windowSize 设为 1 测试一下,100% 会看到占位 View。

4.Blank areas

空白 View,VirtualizedList 会把渲染区域外的 Item 替换为一个空白 View,用来减小长列表的内存占用。顶部和底部均可以有。

上图是渲染图,咱们能够利用 react-devtools 再看看 React 的 Virtual DOM(为了截屏方便,我把 initialNumToRender 和 windowSize 设为 1),能够看出和上面的示意图是一致的。

5.removeClippedSubviews

这个翻译过来叫「裁剪子视图」的属性,文档描述不是很清晰,大意是设为 true 能够提升渲染速度,可是 iOS 上可能会出现 bug。这个属性 VirtualizedList 没有作任何优化,是直接透传给 ScrollView 的。

在 0.59 版本的一次 🔗 commit 里,FlatList 默认 Android 开启此功能,若是你的版本低于 0.59,能够用如下方式开启:

removeClippedSubviews={Platform.OS === 'android'}
复制代码

6.maxToRenderPerBatch 和 updateCellsBatchingPeriod

VirtualizedList 的数据不是一会儿所有渲染的,而是分批次渲染的。这两个属性就是控制增量渲染的。

这两个属性通常是配合着用的,maxToRenderPerBatch 表示每次增量渲染的最大数量,updateCellsBatchingPeriod 表示每次增量渲染的时间间隔

咱们能够调节这两个参数来平衡渲染速度和响应速度。可是,调参做为一门玄学,很可贵出一个统一的「最佳实践」,因此咱们在业务中也没有动过这两个属性,直接用的系统默认值。

2️⃣️ ListLtems 优化

📄 ListLtems 优化 文档reactnative.cn/docs/optimi…

文档中说了好几点优化,其实在前文我都介绍过了,这里再简单提一下:

1.使用 getItemLayout

若是 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就很是的合算。

在源码中(#L1287#L2046),若是不使用 getItemLayout,那么全部的 Cell 的高度,都要调用 View 的 onLayout 动态计算高度,这个运算是须要消耗时间的;若是咱们使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了计算,节省了这部分的开销。

在这里我还想提一下几个注意点,但愿你们使用 getItemLayout 要多注意一下:

  • 若是 ListItem 高度不固定,使用 getItemLayout 返回固定高度时,由于最终渲染高度和预测高度不一致,会出现页面跳动的问题【🔗 问题连接
  • 若是使用了 ItemSeparatorComponent,分隔线的尺寸也要考虑到 offset 的计算中【🔗 文档连接
  • 若是 FlatList 使用的时候使用了 ListHeaderComponent,也要把 Header 的尺寸考虑到 offset 的计算中【🔗 官方示例代码连接

2.Use simple components & Use light components

使用简单组件,核心就是减小逻辑判断和嵌套,优化方式能够参考「2、减轻渲染压力」的内容。

3.Use shouldComponentUpdate

参考「1、re-render」的内容。

4.Use cached optimized images

参考「3、图片优化那些事」的内容。

5.Use keyExtractor or key

常规优化点了,能够看 React 的文档 🔗 列表 & Key

6.Avoid anonymous function on renderItem

renderItem 避免使用匿名函数,参考「4、对象建立调用分离」的内容。


7、React Native 性能优化用到的工具

性能优化工具,本质上仍是调试工具的一个子集。React Native 由于它的特殊性,作一些性能分析和调试时,须要用到 RN/iOS/Android 三端的工具,下面我就列举一下我日常用到的工具,具体的使用方法不是本文的重点,若有须要可根据关键词自行搜索。

1.React Native 官方调试工具

这个官网说的很清楚了,具体内容可见🔗 直达连接

2.react-devtools

React Native 是跑在原生 APP 上的,布局查看不能用浏览器插件,因此要用这个基于 Electron 的 react-devtools。写本文时 React Native 最新版本仍是 0.61,不支持最新 V4 版本的 react-devtools,还得安装旧版本。具体安装方法可见这个🔗 连接

3.XCode

iOS 开发 IDE,查看分析性能问题时能够用 instrumentsProfiler 进行调试。

4.Android Studio

Android 开发 IDE,查看性能的话可使用 Android Profiler🔗 官方网站写的很是详细。

5.iOS Simulator

iOS 模拟器,它的 Debug 能够看一些分析内容。

6.Android 真机 -> 开发者选项

Android 开发者选项有很多东西可看,好比说 GPU 渲染分析和动画调试。真机调试时能够开启配合使用。


8、推荐阅读

【React Native 性能优化指南】到此就算写完了,文中内容可能有不严谨 or 错误的地方,请各位前端/iOS/Android 大佬多多指教。

全文参考近 50 个连接,全放文末太占篇幅了,因此我都分散在文章各处了,我以 emoji 表情🔗 标记的方式进行提示,你们有疑惑的地方能够去原文查看。

在此我还要推荐一下我之前写的关于 Webpack 的文章,两篇都是全网首创


最后推荐一下个人我的公众号,「卤蛋实验室」,平时会分享一些前端技术和数据分析的内容,你们感兴趣的话能够关注一波: