在业务迭代上线的过程中,往往会出现一些代码冗余,导致最终打包出来的 bundle size 不尽如人意。同时,业务包占用的尺寸过大,对应用的性能以及用户体验都会造成一定程度的影响。
本文将从 JavaScript 层面对 React Native 的业务包进行分析与优化,在这个过程中会运用 CRN (Ctrip React Native)bundle 分析平台等工具,在项目开发的中后期对业务包的尺寸进行裁剪优化。
目前针对 React Native 的性能调优可以使用的工具少之又少,下面将介绍 React Native 中可以对 bundle 进行可视化的本地工具,以及我们为什么需要一个在线平台去构建 bundle 分析结果。
在 react-native 中可以使用 react-native-bundle-visualizer 进行 bundle 的查看。它的原理是使用了 source-map-explorer 进行了 Metro bundler 的可视化输出。
Metro 是 React Native 官方的打包程序,会生成对应的 bundle 文件。在 react 中或者是使用 webpack 等工具打包出来的内容,都可以使用与 source-map-explorer 相关的一些打包分析工具进行可视化内容查看。
使用 bundle 分析工具,可以比较明显地辨识出哪些业务文件大小比较异常、需要进行优化,或者是引用了哪些 Javascript 库,导致 bundle 膨胀。
执行如下命令进行安装并启动:
cmd yarn add --dev react-native-bundle-visualizer && yarn run react-native-bundle-visualizer
如果没有跑出具体内容,则需要手动添加入口文件。例如,--entry-file ./index.android.js。执行结果会把 node_modules 和源文件中打包出来的代码尺寸都包含在内,可以清晰地看出哪些文件占用的空间比较大。
Web 端针对 React 的分析优化工具很多,包括 webpack 官方也有提供打包分析,但这些针对 React Native 都不能使用。在上一小节中提到的工具,也只能在本地运行,每次改动后需要生成新的 treemap 进行图片之间的对比查看,不直观并且不方便对比。
React Native 开发的模块最后都会打包到 APP 中,如果能在平时的开发阶段,就注重保持 Bundle SIZE 的简洁,注意观察业务包 SIZE 的限制大小,那么不需要后期进行排查裁剪。
CRN bundle 分析平台不需要使用者手动运行,只需要使用者选择自己的业务包名称,即可进行在线的分析,并且可生成过去 7 天 bundle size 色阶图,可以让使用者对过去一段时间内的开发打包结果进行及时排查,也就是说可以对包内尺寸的膨胀进行告警。
现有的 React Native Bundle 分析工具,除了只能本地进行运行以外,还存在的缺点就是它是针对 React Native 官方的打包工具的运行结果进行的分析,对于 Ctrip React Native 或者是其他基于 React Native 优化的跨平台开发框架,是会有一定缺陷的,例如无法找到正确的入口文件、无法找到对应的依赖关系等等。
针对 React Native 进行 bundle 分析的在线平台,相较于现有的工具,具有以下优点:
定制化的 RN bundle 分析平台,可以随时拉取当前业务包的历史打包结果,并且进行在线分析与告警,还可以让使用者得到一个关于本次优化内容的文件差异对比内容,在分析优化后,可以快速看到优化效果,简单高效。
CRN bundle 分析平台,可以对 React Native 打包后的内容进行在线二次分析。它具有项目内部模块依赖分析、文件尺寸树状结构矩形图等图表展示功能。
如上所示中,展示了业务模块的整体大小以及压缩后的尺寸,并且进行了图形化的占比展示。
在条形图中,从打包的模块内容角度,显示了当前业务包中占比最大的五项内容,包括 build 后生成的内容,以及 node_modules 中的模块大小占比。
底部的占比图中,从文件类型的角度,显示了当前业务包中的 JavaScript、Font、Image 等文件类型的大小占比。
在 bundle 概要的页面,显示了当前业务包的源代码大小以及打包后的压缩大小。
bundle Size 详情页面,使用树形结构图,直观地展示了当前业务包中各个模块的尺寸大小以及占比。
可对相应的模块文件进行搜索查看,同时会高亮展示在树形结构图区域,以便排查和优化打包结果。
使用详情页面,可以对优化前后的结果进行图形化的尺寸对比。
模块依赖分析页面,会根据模块的依赖关系生成 dependency graph,便于排查模块之间的深层依赖。
对于任意两个发布单,可以根据 JobId 进行包内各个文件的大小 diff 对比,并且会链接到 gitlab 对应的 changes 内容,可以看到代码优化部分的相关 commmits 。
使用该功能,可以直观地进行某次裁剪前后的尺寸大小对比,快速验证优化效果。
对于每个业务包可以给出一个可用包尺寸大小,并且根据每日打包结果,生成对应的过去时间段中的打包尺寸大小色阶图,使用色阶可以预警过去的时间段中是否出现超限的业务包打包结果,及时对打包内容进行排查。
CRN bundle 分析平台主要依赖三个部分进行实现,分别是处理 JOB 数据、使用后台 API 分析打包后的业务包文件,最后在前端进行各种图表化的展示。
打开平台页面后,使用者选择要分析的业务包名称,后台 API 根据参数调用相关接口,得到要分析的业务包的下载地址和对应的内容映射文件,并且将数据添加到队列中,等待后续分析处理。
循环调用后台 API 去获取要分析的 JOB 进行数据处理。在这个过程中,调用 Nodejs 对当前选择的业务包进行基础分析,并与 map 文件相结合,得到关键依赖数据与代码详情内容,生成最基础也是最重要的数据包,这个数据包使用 JobId 作为文件名称,得到一个 JSON 格式的数据内容,后续的处理都在这个 JSON 文件的基础上进行。
获得一个基础数据文件后,使用前端把数据处理为需要的格式进行展示。在前端交互逻辑上使用了 Vue.js 与 element-ui 进行基础页面构建,使用 d3.js 进行了数据的可视化展示,在这里用到了树形矩阵图、色阶图、条形图、依赖关系图等等图表进行内容展示。在 DIFF 页面中,同时分析了两个指定的 JobId 下的业务包内容,并且按照差异内容进行了详细的 SIZE 增减对比。
在进行包裁剪之前,我们需要先分析业务包内各模块的占比大小,以便对具体的模块进行修改。工欲善其事,必先利其器。在有了分析工具后,可以对业务包模块进行详细的分析与优化。在这里,使用本地安装的 bundle 分析工具进行普适的分析。
在这个截图中,可以很清楚地看到,除了公共引用库以外的内容中,有几个比较明显的膨胀模块,分别是 lodash、moment,以及一个工具类库下的业务逻辑文件。接下来我们针对这几处明显的问题进行优化。
Momentjs 和 Lodashjs 是前端常用类库,但这两个都有很明显的问题,所占据的文件空间略大,而且大多数时候我们只需要用到其中小部分的功能。在如下类库替换过程中用到的方法,可以运用到所有常用类库的优化使用中。
moment 是一个常用的 JavaScript 日期处理类库,它支持多语言的日期格式。moment 的核心代码只有 52kb,但是包含了全世界语言的本地化文件,也就是说当你使用其中的功能时,也包含了很多你用不到的特性。
对应的解决方案是你可以通过 npm 安装moment-mini,该库非官方维护,但暴露了官方的 moment-min.js 作为 npm 模块开源使用。或者你可以直接使用一些更为简洁的 JavaScript 日期格式化类库。
作为 momentjs 的替代方案,可以使用 luxon、date-fns、dayjs,或者直接使用 JavaScript 的原生 API 来做日期国际化(JavaScript Internationalization API)。如果不需要引入日期国际化,dayjs 核心代码只有 7.1k,可以作为 momentjs 的替代。
lodash 是一个实用性非常高的 JavaScript 工具库,可以对 array、object、string 等值进行操作和检测等等,还具有一些非常实用的函数。但 lodash 类库所占用的空间达到了 71K,而且也存在很多你用不上的方法。实际上,我们在使用中或许只会用到非常少的几个函数。
官方虽然也提供了 lodash-cli 这样的工具,让使用方可以针对具体的某些函数进行打包,但官方是不推荐这种用法的,并且在新的版本中也取消了这样的部分模块打包方式。官方推荐的方式是,在引用时指定对应的函数,这样最终打包时只会打包对应的函数。
如下所示,如果直接引用 lodash,大小时 71K。
javascript import get from 'lodash' // 71K (gzipped: 24.7K)
如果引用对应的函数,那么所需要的空间会大大减少。
javascript import get from 'lodash/get' // 8.2K (gzipped: 2.5K)
babel-plugin-transform-imports
这个插件可以把全局 import 替换为具体模块的单独引入。
配置如下:
# .babelrc
"plugins": [
["transform-imports", {
"lodash": {
"transform": "lodash/${member}",
"preventFullImport": true
}
}]
]
具有如下效果:
import { map, some } from 'lodash'
// 被替换为
import map from 'lodash/map'
import some from 'lodash/some'
注意这个选项 preventFullImport 在引入整个库的时候会让插件抛出异常。
在 ESLint 中配置 no-restricted-imports 规则,也可以在全局引入时抛出异常。
# .eslintrc
"no-restricted-imports": [
"error",
{
"paths": [
"lodash"
]
}
]
在如下引入方式时会抛出异常:
import { map } from 'lodash'
但按照这样编写则不会报错:
import map from 'lodash/map'
具体使用方法可查看该规则说明,可以对引入模块的代码风格进行控制。
使用功能齐全的工具性函数是非常诱人的,可以快速交付,或者是能够对未来的功能进行快速实现。但是过度实现增加了目前不需要的代码,其造成的复杂性,会对 bundle 的大小产生一定的影响。
在我们的项目中使用到的是 Lodash,官方虽然指出只引入对应模块就会便捷很多。但 Lodash 依然有很多存在依赖关系的内部函数需要一起打包进去。如果你仅仅是使用到这个实用库类的部分工具函数,那么可以用一些体积更小的工具包进行优化,或者直接使用对应的原生实现方式进行替换。
如果我们的对于项目代码中的依赖关系,只引入了一小部分相关内容,并且可以在合理的时间内对其进行重写。那么我们应该重写这部分代码,以达到优化冗余代码的目的。把项目中涉及到的工具库类函数直接用原生代码替换,不失为一个很好的解决方案。
以下是原生 JavaScript 实现 Lodash 的 debounce 函数:
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
}
// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', debounce(calculateLayout, 150));
这里的替换掉不必要的组件/模块,更多地是从业务逻辑方面来说的。如果已经引用的库里面存在某些业务逻辑功能,或者有公用的组件已经实现了对应的功能,那么我们应该进行替换,删除掉多余的业务内代码。
同样的,检查下 package.json 文件中也许会存在未使用的包,或者是重复功能。在开发阶段,也许会存在引用了某些库类,随着业务变化,又在具体逻辑中删除了引用,但未清除彻底,导致 package 中还有残余,却给 bundle size 带来了一定的负担。也或者是同上面 lodash 和 moment 库,可以通过用一些更简单的库,或者自己实现几个常用功能来进行整个模块的替换。
在这里对于我们的业务包来说,包内存在以下这些问题:
1)把业务内不必要的组件替换为公用组件
2)删除不必要的 node_modules 模块,或者用其他模块替代
在这个层面上来说,是细粒度上面的代码冗余的清理,包括下线实验代码的处理工作等等。
除了页面展示需要的代码模块以外,不应该加载多余的代码逻辑。对于不同的业务固然有不同的方法,但核心的两个主要方法是:
React Native 官方提供的 require 目前并不支持动态加载,所以 CRN 框架提供了 lazyRequire 来支持懒加载方案。App 组件将在跳转页面的时候再加载该模块。
let page = lazyRequire('./src/Page.js');
const pages = [
{
component: page,
name:'page',
isInitialPage: true
}
];
我们可以通过内联引用的方式,延迟模块或文件的加载,直到实际需要该文件。但如上所说,目前 React Native 并不支持动态加载,所以需要 state 属性去控制是否引入对应模块。
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>
);
}
}
在 bundle 分析截图中,到目前为止虽然已经解决了一些库类引用不合理的问题,但还存在逻辑文件过长的问题,分析后发现是里面包含了超大的 base64 图片。
base64 更适合出现在一些重复使用的背景图片,或者尺寸极小的 ICON 的情形,而一些较大的图片则适合使用 PNG 或者 JPEG 。
PNG 是无损的,JPEG 是有损的。如果不需要背景透明,那么把 PNG 转换为 JPEG 会更节省空间。
1)剪裁图片大小:设计师给出的图片一般会比较大,而实际应用中不需要这么大的图片,可以适当地进行图片大小的裁剪。
2)压缩图片质量:对图片进行无损压缩后,再进行 base64 使用。
经过以上两个步骤以后,base64 的图片字节数会明显减少很多。如果字节数还是很大,那么应该考虑是否不适合使用 base64 进行展示。
在 React Native 的 ESLint 规则中配置 react-native/no-unused-styles ,会检测在 React 组件中存在的未使用 CSS 。
在长期对组件进行开发的过程中,随着 UX/UI 的更改,会存在一些冗余的样式散落在文件中。这样的一个配置可以很好地显示出冗余的部分。
# .eslintrc "rules": { "react-native/no-unused-styles": 2 }
但它存在很明显的缺陷,就是在 css-in-js 的写法中可以检测到当前文件中的 css 对象引用,也就是对于 styles.xxx 可以很好地检测到。
但是对于以下几种,目前这个规则都无法检测:
1)Import 进来的 CSS 文件无法识别;
2)使用 StyleSheet.flatten 等方法操作过的 style 无法识别;
const styles = StyleSheet.flatten([style1, style2])// 无法检测到该对象中存在的样式
const A = StyleSheet.create({
header: {
color: "#61dafb",
fontSize: 30,
marginBottom: 36
}
})
const styles = IS_BOOL ? A : B
// 无法检测到 A/B 中存在的样式
这些问题官方目前都未修复。这个规则只能检测最简单形式的 CSS 内容中的冗余,如果希望一直能使用该规则,则在代码规范上需要保持简洁的 CSS 引用形式。
在经过一系列的分析与细节的优化操作过后,成果是压缩后的 Bundle Size 减少了约 50% 的空间占比。
进行 bundle 分析后,可以明显地找出尺寸异常的文件或者模块,进行对应的优化,从大的层面上进行分析与尺寸优化:
1)根据 bundle 分析裁剪具体模块,分析模块引用代价;
2)替换不合理的库类引用
粗粒度的优化后,剩下的有关逻辑的代码优化,就跟平时的编写有关。从小的层面上进行优化需要:
1)从逻辑上分析不必要存在的库类/模块引用;
2)编写逻辑代码时,需要更加注重保持代码行数的简洁;
3)提取常用功能为公用组件进行使用;
4)静态资源使用优化
在代码编写阶段保持最佳实践是最好的,但在中后期我们也能通过一些分析工具进行代码包的裁剪。项目结构与模块的依赖关系更加复杂的时候,运用以上方案进行 bundle 裁剪会更加有效。
保持 Bundle 尺寸精简是一个长期的任务,就像保持代码简洁一样,甚至可以说这项任务与项目的生命周期是紧密相连的。有了良好的工具可以更加方便开发者进行分析,实时查看目前的代码简洁程度。
作者简介
Sheila,携程资深前端开发工程师,关注前端性能优化;xqin,携程前端开发专家,CRN bundle 分析平台开发者。
本文转载自:携程技术(ID:ctriptech)
领取专属 10元无门槛券
私享最新 技术干货