2020 年谈 React Native,在突飞猛进的前端圈,可能算比较另类了。文章动笔以前我也犹豫过,可是想到写技术文章又不是赶时髦,啥新潮写啥,因此仍是动笔写了这篇 React Native 性能优化的文章。javascript
本文谈到的 React Native 性能优化,还没到修改 React Native 源码那种地步,因此通用性很强,对大部分 RN 开发者来讲都用得着。html
本文的内容,一部分是 React/RN/Android/iOS 官方推荐的优化建议,一部分是啃源码发现的优化点,还有一部分是能够解决一些性能瓶颈的优秀的开源框架。**本文总结的内容你不多在网络上看到,因此看完后必定会有所收获。**若是以为写的不错,请不要吝啬你的赞,把这篇 1w 多字的文章分享出去,让更多的人看到。前端
看文章前要明确一点,一些优化建议并非对全部团队都适用的。有的团队把 React Native 当加强版网页使用,有的团队用 React Native 实现非核心功能,有的团队把 React Native 当核心架构,不一样的定位须要不一样的选型。对于这些场景,我在文中也会提一下,具体使用还须要各位开发者定夺。java
<br/>react
<br/>android
由于 React Native 也是 React 生态系统的一份子,因此不少 React 的优化技巧能够用到这里,因此文章刚开始先从你们最熟悉的地方开始。webpack
对于 React 来讲,减小 re-render 能够说是收益最高的事情了。git
📄 文档: https://react.docschina.org/docs/optimizing-performance.html#shouldcomponentupdate-in-actionsxgithub
简单式例:web
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
必定是座上宾。
咱们经过这个 API,能够拿到先后状态的 state/props,而后手动检查状态是否发生了变动,再根据变动状况来决定组件是否须要从新渲染。
🔗 官方文档对 shouldComponentUpdate
的做用原理和使用场景已经说的很是清晰了,我就没有必要搬运文章了。在实际项目中,阅文集团的 🔗 React Native 应用**「元气阅读」**也作了很好的示范,🔗 Twitter 的性能优化分享也作的图文并茂,可有很高的参考价值,对此感兴趣的同窗能够点击跳转查看。
在此我想提醒的是,shouldComponentUpdate 是强业务逻辑相关的,若是使用这个 API,你必须考虑和此组件相关的全部 props 和 state,若是有遗漏,就有可能出现数据和视图不统一的状况。因此使用的时候必定很是当心。
在此我想提醒的是,shouldComponentUpdate 是强业务逻辑相关的,若是使用这个 API,你必须考虑和此组件相关的全部 props 和 state,若是有遗漏,就有可能出现数据和视图不统一的状况。因此使用的时候必定很是当心。
**📄 文档:**https://react.docschina.org/docs/react-api.html#reactmemo
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()
返回的是 true
,shouldComponentUpdate
却返回的是 false
。
**📄 文档:**https://react.docschina.org/docs/react-api.html#reactpurecomponent
简单式例:
class PureComponentButton extends React.PureComponent { render() { return <button color={this.props.color} />; } }
和 shouldComponentUpdate
相对应,React 还有一个相似的组件 React.PureComponent
,在组件更新前对 props 和 state 作一次浅比较。因此涉及数据嵌套层级过多时,好比说你 props 传入了一个两层嵌套的 Object,这时候 shouldComponentUpdate
就很为难了:我究竟是更新呢仍是不更新呢?
考虑到上面的状况,我在项目中通常不多用 PureComponent
。虽然很简单易用,可是面对复杂逻辑时,反而不如利用 shouldComponentUpdate
手动管理简单粗暴。固然这个只是我的的开发习惯,社区上也有其余的解决方案:
PureComponent
进行渲染时机的管理PureComponent
进行数据比较(🔗 参考连接:有赞 React 优化)在这个问题上仁者见仁智者见智,在不影响功能的前提下,主要是看团队选型,只要提早约定好,其实在平常开发中工做量都是差很少的(毕竟不是每一个页面都有必要进行性能优化)。
<br/>
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 时,并非一一对应的,咱们了解了这些知识后,能够如何优化布局呢?
📄 React Fragments 文档:https://zh-hans.reactjs.org/docs/fragments.html
咱们先从最熟悉的地方讲起——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 嵌套层级。
咱们在业务开发时,常常会遇到这种场景:整个界面的背景色是白色的,上面又加了一个白色背景的卡片组件,卡片内部又包含了一个白色背景的小组件......
// 如下示例 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 来讲,出现红色区域,就说明出现了颜色混合:
对于 Android 来讲,GPU 会画蛇添足地渲染对用户不可见的像素。有一个颜色指示条:白 -> 蓝 -> 绿 -> 粉 -> 红
,颜色越日后表示过分绘制越严重。
在过渡绘制这个测试上,iOS 和 Android 的实验结果几乎是彻底相反的,因此解决方案确定不是一箭双鵰的,我我的认为,React Native 开发作视图优化时,应该优先优化 Android,因此咱们能够从如下几点优化:
避免 GPU 过分绘制的细节太多了,通常页面不须要这种精细化管理,长列表优化时能够考虑一下这个方向。
<br/>
性能优化的另外一个大头就是图片。这里的图片优化不只仅指减小图片大小,减小 HTTP 带宽占用,我会更多的讨论一些 Image 组件上的优化,好比说缓存控制,图片采样等技术。
React Native 的 Image
图片组件,若是只是做为普通的图片展现组件,那它该有的都有了,好比说:
onLoadStart
/onLoad
/onLoadEnd
/onError
可是,若是你要把它当一个图片下载管理库用时,就会很是的难受,由于 Image 的这几个属性在 iOS/Android 上有不一样的表现,有的实现了有的没有实现,用起来很是不顺手。
在讲解图片优化前,咱们先想一下,一个基本的图片下载管理库要实现什么:
针对上面的 4 条原则,咱们来一一刨析 Image
组件。
基础的 png
/jpg
/base64
/gif
格式,支持良好。不过要注意的是,想要 Android 加载的 gif
图片动起来,要在 build.gradle
里面加一些依赖,具体内容能够看这个 🔗 连接。
若是要加载 webp 格式的图片,就有些问题了。做为 Google 推出的一种图片格式,Android 天然是支持的,可是 iOS 就不支持了,须要咱们安装一些第三方插件。
先说结论,Image
组件对图片的下载管理能力基本为 0。
Image
基本上只能监听单张图片的加载流程:onLoadStart
/onLoad
/onLoadEnd
/onError
,若是要控制多张图片的下载优先级,对不起,没有。
缓存这里要从两方面说,一是经过 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 缓存文档。
都快到 5G 时代了,短视频/VLog 你们都每天刷了,更不用说多图场景了,基本上已是互联网应用的标配了。
讲图片加载前先明确一个概念:图片文件大小 != 图片加载到内存后的大小。
咱们常说的 jpg png webp,都是原图压缩后的文件,利于磁盘存储和网络传播,可是在屏幕上展现出来时,就要恢复为原始尺寸了。
好比说一张 100x100 的 jpg 图片,可能磁盘空间就几 kb,不考虑分辨率等问题,加载到内存里,就要占用 3.66 Mb。
// 不一样的分辨率/文件夹/编码格式,都会带来数值差别 // 下面的计算只是最通常的场景,领会精神便可 (100 * 100 * 3) / (8 * 1024) = 3.66 Mb (长 * 宽 * 每一个像素占用字节数) / (8 * 1024) = 3.66 Mb
上面只是 100x100 的图片,若是图片尺寸增长一倍,图片在内存里的大小是按平方倍数增加的,数量一多后,内存占用仍是很恐怖的。
在多图加载的场景里,通过实践,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,来关闭这个加载动画。
📄 色彩深度 wiki:https://github.com/DylanVann/react-native-fast-image/blob/master/README.md
色彩深度这个概念其实前面也提了一下,好比说咱们经常使用的带透明度 PNG 图片,就是 32 位的:
为啥推荐使用 32 bit 图片呢?直接缘由有 2 个:
<br/>
虽然推荐 32 bit 图片,可是说实话,这个对前端开发是不可控的,由于图片来源通常就 2 个:
因此想针对这一点进行优化的话,沟通成本挺高,收益反而不高(通常只在长列表有些问题),但也是图片优化的一个思路,故放在这一节里。
前面举了一个 100x100 的 ImageView 加载 1000x1000 Image 致使 Android 内存 OOM 的问题,咱们提出了设置 resizeMethod={'resize'}
的方法来缩减图片在内存中的体积。其实这是一种无奈之举,若是能够控制加载图片的大小,咱们应该保持 Image 和 ImageView 长宽一致。
首先咱们看看长宽不一致会引发的问题:
React Native 开发时,布局使用的单位是 pt,和 px 存在一个倍数关系。在加载网络图片时,咱们可使用 React Native 的 🔗 PixelRatio.getPixelSizeForLayoutSize 方法,根据不一样的分辨率加载不一样尺寸的图片,保证 Image 和 ImageView 长宽一致。
📄 react-native-fast-image 文档:https://github.com/DylanVann/react-native-fast-image/blob/master/README.md
通过上面的几个 Image 属性分析,综合来看,Image 组件对图片的管理能力仍是比较弱的,社区上有个 Image 组件的替代品:react-native-fast-image。
它的底层用的是 🔗 iOS 的 SDWebImage 和 🔗 Android 的 Glide 。这两个明星图片下载管理库,原生开发同窗确定很熟悉,在缓存管理,加载优先级和内存优化上都有不错的表现。并且这些属性都是双平台可用,这个库都封装好了,可是官网上只有基础功能的安装和配置,若是想引入一些功能(好比说支持 WebP),仍是须要查看 SDWebImage 和 Glide 的文档的。
引入前我仍是想提醒一下,React Native 的 Android Image 组件底层封装了 FaceBook 的 Fresco,引入这个库至关于又引入了 Glide,包体积不可避免的会变大,因此引入以前可能还要均衡一下。
前面说的都是从 React Native 侧优化图片,可是一个产品历来不是单打独斗,借助服务端的力量其实能够省不少事。
WebP 的优点不用我多说,一样的视觉效果,图片体积会明显减小。并且能够显著减少 CodePush 热更新包的体积(热更新包里,图片占用 90% 以上的体积)。
虽然 WebP 在前端解压耗时可能会多一点点,可是考虑到传输体积缩小会缩短网络下载时间,总体的收益仍是不错的。
通常比较大的企业都有内建图床和 CDN 服务,会提供一些自定制图片的功能,好比说指定图片宽高,控制图片质量。固然一些比较优秀的第三方对象存储也提供这些功能,好比说🔗 七牛云 图片处理。
借用云端图片定制功能,前端能够轻松经过控制 URL 参数控制图片属性。
好比说 Android 经过 resizeMethod
的 resize
更改图片字节大小,虽然也能够解决问题,可是这个算法仍是在前端运行的,仍是会占用用户内存资源。咱们把连接改为:
https://www.imagescloud.com/image.jpg/0/w/100/h/100/q/80 // w: 长为 100 px // h: 宽最多为 100 px // q: 压缩质量为 80
这样子就能够把计算转移到服务端,减小前端的 CPU 占用,优化前端总体的性能。
<br/>
对象建立和调用分离,其实更多的是一种编码习惯。
咱们知道在 JavaScript 里,啥都是对象,而在 JS 引擎里,建立一个对象的时间差很少是调用一个已存在对象的 10 多倍。在绝大部分状况下,这点儿性能消耗和时间消耗根本不值一提。但在这里仍是要总结一下,由于这个思惟习惯仍是很重要的。
📄 文档:https://zh-hans.reactjs.org/docs/handling-events.html
做为一个前端应用,除了渲染界面,另外一个重要的事情就是处理用户交互,监听各类事件。因此在组件上绑定各类处理事件也是一个优化点。
在 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 一次就要建立一个新的函数是真的不必。
这个其实和第一个差很少,只不过把事件回调函数改为渲染函数,在 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} /> }
一样的道理,ListHeaderComponent
和 ListFooterComponent
也应该用这样写法,预先传入已经渲染好的 Element,避免 re-render 时从新生成渲染函数,形成组件内部图片从新加载出现的闪烁现象。
📄 文档:https://reactnative.cn/docs/stylesheet/
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
,通用性和高性能不能同时兼得,根据不一样的业务场景采起不一样的方案才是正解。
咱们写代码时,为了不传入 []
的地方因数据没拿到传入 undefined
,常常会默认传入一个空数组:
render() { return <ListComponent listData={this.props.list || []}/> }
其实更好的作法是下面这样的:
const EMPTY_ARRAY = []; render() { return <ListComponent listData={this.props.list || EMPTY_ARRAY}/> }
这个其实算不上啥性能优化,仍是前面再三强调的思路:对象建立和调用分离。毕竟每次渲染的时候从新建立一个空的数组/对象,能带来多大的性能问题?
把 []
改成统一的 EMPTY_ARRAY
常量,其实和平常编码中避免出现 Magic Number 同样,算一种编程习惯,但我以为这种优化能够归到这个类别里,因此专门提一下。
<br/>
动画流畅很简单,在大部分的设备上,只要保证 60fps 的帧率就能够了。但要达到这个目标,在 React Native 上仍是有些问题的,我画了一张图,描述了目前 React Native 的基础架构(0.61 版本)。
上图咱们能够很容易的看出,JS 线程太忙了,要作的事情太多了。并且 UI Thread 和 JS Thread 以前通讯是异步的(Async Bridge),只要其它任务一多,就很难保证每一帧都是及时渲染的。
分析清楚了,React Native 动画优化的方向天然而然就出来了:
useNativeDrive: true
📄 文档:https://facebook.github.io/react-native/docs/animations#using-the-native-driver
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
这个属性也有着局限性,只能使用到只有非布局相关的动画属性上,例如 transform
和 opacity
。布局相关的属性,好比说 height 和 position 相关的属性,开启后会报错。并且前面也说了,useNativeDriver
只能用在可预测的动画上,好比说跟随手势这种动画,useNativeDriver
就用不了的。
setNativeProps
📄 文档:https://facebook.github.io/react-native/docs/direct-manipulation
setNativeProps
这个属性,至关于直接操做浏览器的 DOM。React 官方通常是不推荐直接操做 DOM 的,但业务场景变幻无穷,总会遇到一些场景不得不操做 DOM,在React Native 里也是一样的道理。
好比说下面的动图,在屏幕中上下滚动时,y 轴上的偏移能够经过 ScrollView#onScroll
属性开启 useNativeDrive: true
来优化滚动体验。可是咱们能够看到,随着上下滑动,圆圈里的数字也是随之变化的。
若是把数字存在 this.state
里, 每次滑动不可避免的要进行大量的 setState
,React 端会进行大量的重绘操做,可能会引发掉帧。咱们这里就能够用 setNativeProps
,避免 React 端重绘,至关于直接修改 DOM 上的数字,这样可让动画更加流畅。
📄 文档:https://facebook.github.io/react-native/docs/interactionmanager
原生应用感受如此流畅的一个重要缘由就是在互动和动画的过程当中避免繁重的操做。
在 React Native 里,JS 线程太忙了,啥都要干,咱们能够把一些繁重的任务放在 InteractionManager.runAfterInteractions()
里,确保在执行前全部的交互和动画都已经处理完毕。
InteractionManager.runAfterInteractions(() => { // ...须要长时间同步执行的任务... });
在 React Native 官方提供的组件里,PanResponder、Animated,VirtualizedList 都用了 InteractionManager,为的就是平衡复杂任务和交互动画之间的执行时机。
📺 视频教程:https://www.youtube.com/channel/UC806fwFWpiLQV5y-qifzHnA
📄 react-native-gesture-handler 文档:https://github.com/software-mansion/react-native-gesture-handler
📄 react-native-reanimated 文档:https://github.com/software-mansion/react-native-reanimated
这两个库是被 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-handler 和 react-native-reanimated,是一个不错的选择,能够大幅度提升动画的流畅度。
📄 BindingX 文档:https://alibaba.github.io/bindingx/guide/cn_introduce
BindingX 是阿里开源的一个框架,用来解决 weex
和 React Native
上富交互问题,核心思路是将"交互行为"以表达式的方式描述,并提早预置到 Native,避免在行为触发时 JS 与 Native 的频繁通讯。
固然,引入上面几个第三方库会确定会带来必定的学习成本。对于复杂交互的页面,有的团队可能会采用原生组件来代替,好比说🔗 美团外卖就会用原生组件去实现精细动画和强交互模块,因此具体使用还要看团队的技术储备和 APP 场景。
<br/>
在 React Native 开发中,最容易遇到的对性能有必定要求场景就是长列表了。在平常业务实践中,优化作好后,千条数据渲染仍是没啥问题的。
虚拟列表前端一直是个经典的话题,核心思想也很简单:只渲染当前展现和即将展现的 View,距离远的 View 用空白 View 展现,从而减小长列表的内存占用。
在 React Native 官网上,🔗 列表配置优化其实说的很好了,咱们基本上只要了解清楚几个配置项,而后灵活配置就好。可是问题就出在「了解清楚」这四个字上,本节我会结合图文,给你们讲述清楚这几个配置。
React Native 有好几个列表组件,先简单介绍一下:
还有一些其余依赖文件,有个🔗 博文的图总结的挺好的,我这里借用它的图一下:
咱们能够看出 VirtualizedList 才是主演,下面咱们结合一些示例代码,分析它的配置项。
讲以前先写个小 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,各个属性咱们再用文字描述一下:
首批应该渲染的元素数量,刚刚盖住首屏最好。并且从 debug 指示条能够看出,这批元素会一直存在于内存中。
视口高度,就是用户能看到内容,通常就是设备高度。
渲染区域高度,通常为 Viewport 的整数倍。这里我设置为 3,从 debug 指示条能够看出,它的高度是 Viewport 的 3 倍,上面扩展 1 个屏幕高度,下面扩展 1 个屏幕高度。在这个区域里的内容都会保存在内存里。
将 windowSize 设置为一个较小值,能有减少内存消耗并提升性能,可是快速滚动列表时,遇到未渲染的内容的概率会增大,会看到占位的白色 View。你们能够把 windowSize 设为 1 测试一下,100% 会看到占位 View。
空白 View,VirtualizedList 会把渲染区域外的 Item 替换为一个空白 View,用来减小长列表的内存占用。顶部和底部均可以有。
上图是渲染图,咱们能够利用 react-devtools 再看看 React 的 Virtual DOM(为了截屏方便,我把 initialNumToRender 和 windowSize 设为 1),能够看出和上面的示意图是一致的。
这个翻译过来叫「裁剪子视图」的属性,文档描述不是很清晰,大意是设为 true 能够提升渲染速度,可是 iOS 上可能会出现 bug。这个属性 VirtualizedList 没有作任何优化,是直接透传给 ScrollView 的。
在 0.59 版本的一次 🔗 commit 里,FlatList 默认 Android 开启此功能,若是你的版本低于 0.59,能够用如下方式开启:
removeClippedSubviews={Platform.OS === 'android'}
VirtualizedList 的数据不是一会儿所有渲染的,而是分批次渲染的。这两个属性就是控制增量渲染的。
这两个属性通常是配合着用的,maxToRenderPerBatch 表示每次增量渲染的最大数量,updateCellsBatchingPeriod 表示每次增量渲染的时间间隔。
咱们能够调节这两个参数来平衡渲染速度和响应速度。可是,调参做为一门玄学,很可贵出一个统一的「最佳实践」,因此咱们在业务中也没有动过这两个属性,直接用的系统默认值。
📄 ListLtems 优化 文档:https://reactnative.cn/docs/optimizing-flatlist-configuration/#list-items
文档中说了好几点优化,其实在前文我都介绍过了,这里再简单提一下:
若是 FlatList(VirtualizedList)的 ListLtem 高度是固定的,那么使用 getItemLayout 就很是的合算。
在源码中(#L1287、#L2046),若是不使用 getItemLayout,那么全部的 Cell 的高度,都要调用 View 的 onLayout 动态计算高度,这个运算是须要消耗时间的;若是咱们使用了 getItemLayout,VirtualizedList 就直接知道了 Cell 的高度和偏移量,省去了计算,节省了这部分的开销。
在这里我还想提一下几个注意点,但愿你们使用 getItemLayout 要多注意一下:
ItemSeparatorComponent
,分隔线的尺寸也要考虑到 offset 的计算中【🔗 文档连接】ListHeaderComponent
,也要把 Header 的尺寸考虑到 offset 的计算中【🔗 官方示例代码连接】使用简单组件,核心就是减小逻辑判断和嵌套,优化方式能够参考「2、减轻渲染压力」的内容。
参考「1、re-render」的内容。
参考「3、图片优化那些事」的内容。
常规优化点了,能够看 React 的文档 🔗 列表 & Key。
renderItem 避免使用匿名函数,参考「4、对象建立调用分离」的内容。
<br/>
性能优化工具,本质上仍是调试工具的一个子集。React Native 由于它的特殊性,作一些性能分析和调试时,须要用到 RN/iOS/Android 三端的工具,下面我就列举一下我日常用到的工具,具体的使用方法不是本文的重点,若有须要可根据关键词自行搜索。
这个官网说的很清楚了,具体内容可见🔗 直达连接。
React Native 是跑在原生 APP 上的,布局查看不能用浏览器插件,因此要用这个基于 Electron 的 react-devtools。写本文时 React Native 最新版本仍是 0.61,不支持最新 V4 版本的 react-devtools,还得安装旧版本。具体安装方法可见这个🔗 连接。
iOS 开发 IDE,查看分析性能问题时能够用 instruments 和 Profiler 进行调试。
Android 开发 IDE,查看性能的话可使用 Android Profiler,🔗 官方网站写的很是详细。
iOS 模拟器,它的 Debug 能够看一些分析内容。
Android 开发者选项有很多东西可看,好比说 GPU 渲染分析和动画调试。真机调试时能够开启配合使用。
<br/>
【React Native 性能优化指南】到此就算写完了,文中内容可能有不严谨 or 错误的地方,请各位前端/iOS/Android 大佬多多指教。
全文参考近 50 个连接,全放文末太占篇幅了,因此我都分散在文章各处了,我以 emoji 表情🔗标记的方式进行提示,你们有疑惑的地方能够去原文查看。
在此我还要推荐一下我之前写的关于 Webpack 的文章,两篇都是全网首创:
最后推荐一下个人我的公众号,「卤代烃实验室」,会讲一些技术和技术以外的内容,你们感兴趣的话能够关注一波:
原文出处:https://www.cnblogs.com/skychx/p/react-native-performance-optimization-guide.html