本文主要介绍
- React Native新旧框架对比
- React与React Native区别
- React Native性能优化
其中第3点React Native性能优化的拆包分包,是项目实战中使用过的,在这里整理分享,如果没有用过的小伙伴会觉得晦涩难懂,建议按照在实际项目中需要去实践,纸上得来终觉浅,绝知此事要躬行。
一、jsBridge
在React Native 中,web和native通过jsBridge进行通信,以js引擎或webview容器作为媒介,通过协定协议进行通信,实现native端和web端双向通信的一种机制。
web
端调用native
端主要的方法:
1、拦截webview请求的URL_Schema
如果符合我们自定义的URL_Schema,进行拦截,拿到相关操作,从而操作native;
如果不符合自定义的URL_Schema,直接转发,请求真正的服务。
2、向webview中注入JS API
通过webview提供的接口,App将Native的相关接口注入到JS的Content对象中,一般来说这个对象内的方法名与native内方法名相同,Web端就可以在全局下使用暴露的js对象,进而调用原生端方法。
二、React Native旧架构的问题
1、消息通知是异步的,对于连续的手势操作可能产生卡顿
2、JSON本身在性能上也存在问题,JSON parse、JSON stringfy类似于深拷贝,耗费内存
3、消息的通信,是在一个channel里,异步通信可能会阻塞。
在我们刚做过的一个大型RN项目中,遇到的直接问题有:
1、性能和问题反馈最大的组件
-
ScrollView:一次渲染不做任何回收,启动性能慢,占用内存大
-
FlatList:做了组件回收,快速滑动中容易出现白屏和卡顿
-
Shadow层最终呈现到原生的UI是异步的,滑动太快会有大量的UI没加载
2、ui渲染阻塞
整体UI渲染太过于依赖JsBridge,容易出现阻塞影响整体UI体验
3、时间阻塞
事件阻塞在JsBridge,导致了长时间白屏的出现
三、React Native新架构的变化
新架构放弃了"桥"的概念,转而采用另一种通信机制:JavaScript 接口(JSI
)。JSI 是一个接口,允许 JavaScript 对象持有对 C++ 的引用,反之亦然。
一旦一个对象拥有另一个对象的引用,它就可以直接调用该对象的方法。例如一个 C++ 对象现在可以直接调用一个 JavaScript 对象在 JavaScript 环境中执行一个方法,反之亦然。
这个想法可以带来几个好处:
- 同步执行:
现在可以同步执行那些本来就不应该是异步的函数。
- 并发:
可以在 JavaScript 中调用在不同线程上执行的函数。
- 更低的开销:
新架构不需要再对数据进行序列化/反序列化,因此可以避免序列化的开销。
- 代码共享:
通过引入 C++,现在有可能抽象出所有与平台无关的代码,并在平台之间轻松共享它。
- 类型安全:
为了确保 JS 可以正确调用 C++ 对象的方法,反之亦然,因此增加了一层自动生成的代码。这些代码必须通过 Flow 或 TypeScript 类型化的 JS 规范来生成。
这些优势是TurboModule系统的基础,也是进一步增强功能的跳板。例如,我们有可能开发出一种新的渲染器,它的速度更快,性能更强:Fabric及其Fabric 组件。
新架构使用了JSI、Hermes 引擎、 Fabric和TurboModules
,旨在提高性能和开发效率,其中一个重要的改进就是允许 JS 和 Native 之间更直接的调用
,减少了不必要的消息通信。
1. JSI
新的架构使用JSI屏蔽js引擎的差异,允许用不同的js引擎,比如Hermes
有了JSI之后,js能持有C++对象的引用,从而允许js和native之间直接调用
,而不用通过跨线程消息通信。
-
一个用 C++ 写成的轻量级框架
-
实现JS引擎的互换
-
通过JS直接调用Native
-
减少不必要的线程通信,省去序列化和反序列化的成本,
-
减轻了通信压力,提高了通信性能
2. Hermes 引擎
Hermes 是一个专为 React Native 优化的 JavaScript 引擎。它通过优化字节码执行和减少内存占用
等方式来提高 JavaScript 代码的执行速度。
Hermes 对代码的预编译和优化
减少了在运行时的解释开销,使得 JavaScript 代码的执行更加高效,这在一定程度上为更直接的交互提供了基础。
3.重构Bridge
新的Bridge被划分为Fabric和TurboModules
- Fabric:负责管理UI
- TurboModules:负责与Native交互
4. Fabric 渲染器
Fabric 是 React Native 的新渲染器。它重新设计了 React Native 的渲染体系结构,使渲染过程更加高效和可预测。
在 Fabric 中,JS 和 Native 之间的交互通过共享内存和直接函数调用等机制
来实现,而不是完全依赖于消息传递。例如,对于一些常见的 UI 操作和交互,JS 端可以直接调用 Native 层的某些函数来触发相应的操作,而不需要经过复杂的消息序列化和传递过程。
Fabric 还采用了异步渲染和可中断渲染
等技术,进一步提高了应用的响应性和性能。
例如,在处理 UI 动画时,以前可能需要通过消息将动画的参数从 JS 传递到 Native 层,然后 Native 层再进行动画的渲染。在新架构下,JS 可以直接调用 Native 层的动画函数,并传递参数,Native 层可以直接响应并开始动画渲染,减少了中间的消息传递环节,提高了动画的流畅性和响应速度。
5. TurboModules
实现Native模块按需加载,并在初始化后直接持有其引用,不再依靠消息通信来调用模块功能。
总的来说,React Native 新架构重新设计了 JS 和 Native 之间的交互方式,使得它们之间可以更直接地进行调用,从而提高了应用的性能和开发体验。
四、React 和 React Native 的概念区别
- React:是一个用于构建用户界面的 JavaScript 库,主要用于在网页开发中构建高效、动态的用户界面,运行在浏览器环境中。
- React Native:是基于 React 开发的用于构建原生移动应用的框架。它允许开发者使用 JavaScript 和 React 编写应用,然后将其编译为原生代码,运行在 iOS 和 Android 等移动平台上。
1、移动应用
import {AppRegistry} from 'react-native';
import App from './App';
AppRegistry.registerComponent('YourApp', () => App);
在 React Native 中,入口文件通常会使用 AppRegistry.registerComponent
方法来注册应用的根组件。
2、移动组件
import React from 'react';
import { Text } from 'react-native';
function MobileComponent() {
return (
<Text>This is a mobile component.</Text>
);
}
export default MobileComponent;
可以看到,React Native 组件中使用的是 react-native
提供的组件(如 Text
),而 React 网页组件通常使用 HTML 元素或自定义的 React 组件。
3、移动样式
在网页开发中,可以使用 CSS 文件来定义样式,并通过 className
来应用样式。
import React from 'react';
import { StyleSheet, Text } from 'react-native';
const styles = StyleSheet.create({
mobileStyle: {
color: 'blue',
fontSize: 18
}
});
function MobileComponentWithStyles() {
return (
<Text style={styles.mobileStyle}>Mobile styled component</Text>
);
}
export default MobileComponentWithStyles;
在 React Native 中,使用 StyleSheet.create
方法来创建样式对象,并将其应用到组件的 style
属性上。
4、尺寸没有单位
所有的尺寸都是没有单位的,我们可以通过实现rem公共方法或者hooks在项目中使用
以下是一种在 React Native 中实现类似 REM 方案的示例方法:
1、创建一个工具函数来计算尺寸
import { Dimensions } from 'react-native';
// 定义一个基础尺寸,比如以设计稿宽度为 750px,基础尺寸为 100px
const baseWidth = 750;
const baseFontSize = 100;
const rem = (size) => {
// 根据当前设备宽度与设计稿宽度的比例来计算
const deviceWidth = Dimensions.get('window').width;
const ratio = deviceWidth / baseWidth;
return size * ratio;
};
在 React Native 项目中,当你导入 react-native 后,就可以访问 Dimensions
对象。它提供了有关设备屏幕尺寸和布局的信息。
2、 在组件中使用
import React from 'react';
import { View, Text, Dimensions } from 'react-native';
import { rem } from './yourUtilsFile';
const YourComponent = () => {
return (
<View style={{ padding: rem(16) }}>
<Text style={{ fontSize: rem(20) }}>Your Text</Text>
</View>
);
};
export default YourComponent;
这里使用了 react-native
的 Dimensions
API 来获取设备屏幕的宽度,然后根据设计稿的宽度和设定的基础字体大小来计算出相应的比例,再根据这个比例来动态调整尺寸。
请注意,这只是一个简单的示例,实际应用中你可能需要根据具体的设计需求和项目结构进行适当的调整和优化。
5、默认flex布局
- 默认flex布局,因此不需要再设置
{display: flex}
- flex的默认方向是column
React Native 中使用 flexbox 规则来指定某个组件的子元素的布局。Flexbox 可以在不同屏幕尺寸上提供一致的布局结构。
一般来说,使用flexDirection
、alignItems
和 justifyContent
三个样式属性就已经能满足大多数布局需求。
1、flexDirection
可以决定布局的主轴。子元素是应该沿着**水平轴(row)方向排列,还是沿着竖直轴(column)方向排列呢;默认值是竖直轴(column)**方向
2、alignItems
决定其子元素沿着次轴(与主轴垂直的轴,比如若主轴方向为row,则次轴方向为column)的排列方式。子元素是应该靠近次轴的起始端还是末尾段分布呢,亦或应该均匀分布
3、justifyContent
子元素沿着主轴的排列方式。子元素是应该靠近主轴的起始端还是末尾段分布,亦或应该均匀分布
详细的flex内容可在react native官网查询使用:
react-native-flex
6、独有的组件
React Native 控件很多,主要分为三类
(1)基础控件
(2)交互控件
(3)列表控件
在React Native 官网中对每一个控件都有详细的介绍:
React Native控件介绍
五、React Native性能优化
1、拆包、分包
React Native 页面的 JavaScript 代码包是热更新平台根据版本号进行下发的,每次有业务改动,我们都需要通过网络请求更新代码包。
因此,我们在对JavaScript 代码进行打包的时候,需要将包拆分成两个部分:
1.1 Common 部分,也就是 React Native 源码部分;
1.2 业务代码部分,也就是我们需要动态下载的部分
具体操作:
- JavaScript 代码包中 React Native 源码相关的部分是不会发生变化的,所以我们不需要在每次业务包更新的时候都进行下发,在工程中内置一份就好了。
- Common 包内置到工程中
- 业务代码包进行动态下载
- 利用 JSContext 环境,在进入载体页后在环境中先加载 Common 包,再加载业务代码包就可以完整的渲染出 React Native 页面
我这里使用的是基于Metro的分包方法,也是市面上主要使用的方法。这个拆包方法主要关注的是Metro工具在“序列化”阶段时调用的 createModuleIdFactory(path)方法和processModuleFilter(module)。createModuleIdFactory(path)是传入的模块绝对路径path,并为该模块返回一个唯一的Id。processModuleFilter(module)则可以实现对模块进行过滤,使业务模块的内容不会被写到common模块里。接下来分具体步骤和代码进行讲解。
(1)metro.common.js配置common包
request('react');
request('react-native');
(2)使用moduleId作为标识避免冲突
Metro 以这个 common.js 为入口文件,打一个 common bundle 包,同时要记录所有的公有模块的 moduleId,但是存在一个问题:每次启动 Metro 打包的时候,moduleId 都是从 0 开始自增,这样会导致不同的 JSBundle ID 重复,为了避免 id 重复,目前业内主流的做法是把模块的路径当作 moduleId
(因为模块的路径基本上是固定且不冲突的),这样就解决了 id 冲突的问题。Metro 暴露了 createModuleIdFactory
这个函数,我们可以在这个函数里覆盖原来的自增逻辑,把公有模块的 moduleId 写入 txt文件
(3)在package.json里配置命令
"build:common:ios": "rimraf moduleIds_ios.txt && react-native bundle --entry-file common.js --platform ios --config metro.common.config.ios.js --dev false --assets-dest ./bundles/ios --bundle-output ./bundles/ios/common.ios.jsbundle",
(4)配置metro.common.config.js
const fs = require('fs');
const pathSep = require('path').sep;
function createModuleId(path) {
const projectRootPath = __dirname;
let moduleId = path.substr(projectRootPath.length + 1);
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
moduleId = moduleId.replace(regExp, '__');
return moduleId;
}
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
return function (path) {
const moduleId = createModuleId(path)
fs.appendFileSync('./moduleIds_ios.txt', `${moduleId}\n`);
return moduleId;
};
},
},
};
生成的moduleIds_ios.txt文件
common.js
node_modules__react__index.js
node_modules__react__cjs__react.production.min.js
node_modules__object-assign__index.js
node_modules__react-native__index.js
node_modules__react-native__Libraries__Components__AccessibilityInfo__AccessibilityInfo.ios.js
......
(5)配置metro.business.config.js
这个步骤的关键在于过滤公有模块的 moduleId(公有模块的Id已经记录在了上一步的moduleIds_ios.txt中),Metro 提供了 processModuleFilter 这个方法,借助它可以实现模块的过滤。这部分的处理主要写在了metro.business.config.ios.js文件中,写在哪个文件中主要取决于最上面package.json命令里指定的文件。
const fs = require('fs');
const pathSep = require('path').sep;
const moduleIds = fs.readFileSync('./moduleIds_ios.txt', 'utf8').toString().split('\n');
function createModuleId(path) {
const projectRootPath = __dirname;
let moduleId = path.substr(projectRootPath.length + 1);
let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
moduleId = moduleId.replace(regExp, '__');
return moduleId;
}
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
serializer: {
createModuleIdFactory: function () {
return createModuleId;
},
processModuleFilter: function (modules) {
const mouduleId = createModuleId(modules.path);
if (modules.path == '__prelude__') {
return false
}
if (mouduleId == 'node_modules__metro-runtime__src__polyfills__require.js') {
return false
}
if (moduleIds.indexOf(mouduleId) < 0) {
return true;
}
return false;
},
getPolyfills: function() {
return []
}
},
resolver: {
sourceExts: ['jsx', 'js', 'ts', 'tsx', 'cjs', 'mjs'],
},
};
(6)最后一步
原生app端需要配置,先读取common包再读取bussiness包
2、拆分业务包
通过拆包的方案,已经减少了动态下载的业务代码包的大小。但是还会存在部分业务非常庞大,拆包后业务代码包的大小依然很大的情况,依然会导致下载速度较慢,并且还会受网络情况的影响。
因此,我们可以再次针对业务代码包进行拆分
将一个业务代码包拆分为一个主包和多个子包的方式
在进入页面后优先请求主包的 JavaScript 代码资源,能够快速地渲染首屏页面,
紧接着用户点击某一个模块时,再继续下载对应模块的代码包并进行渲染,就能再进一步减少加载时间。
3、常规优化
React Native常规的优化方案和react的优化方案是重叠的,我专门写了一篇:
前端宝典之七:React性能优化实战精华篇
文中涵盖了React性能优化的三大方面公11项优化方案,基本涵盖了当今主流优化方案,大家可以前往查看。