本人曾经有过半年多Taro开发小程序的经验,当时使用的是Taro@3
、react@16
、mobx@4
开发微信小程序和支付宝小程序,今天梳理了曾经过做的一些Taro小程序优化工作。
众所周知h5的性能不如原生开发,这是由于
WEB
特性决定,JS
必须是解释型语言(边下载边解析)这就导致JS
执行明显比原生编译型语言慢很多web
技术的跨平台特性要求但是很多人觉得小程序的性能比h5性能好, 原因是 JS是单线程,JS执行与UI渲染互斥,而小程序的双线程机制使得JS实行和UI渲染可以并行执行,所以小程序性能优于h5性能。
但事实上很多小程序页面的加载和运行体验不如h5,问题就出现在这个双线程,它也造成小程序性能瓶颈的原因 -- JS Bridge通信瓶颈,官方说 要控制setData的频次和频率,如果在项目开发过程中如果不注意这方面的问题,就会使得小程序开发的页面不如h5流畅。
双线程机制还带来的一个问题是一些复杂的效果在小程序端实现起来比较困难甚至难以实现, 比如《Taro实现一个出行小程序拖拽滑动的特效》 。尽管小程序官方提供了一些方案,如WXS
、动画、<MovableArea> / <MovableView>
,这无疑也使得开发墒增。
其实小程序设计这个双线程机制主要为了做安全管控
但是,如果小程序一些方案 如缓存优化,分包策略、Skyline引擎等,如果利用得当,开发相同功能的页面会比h5页面性能更好。况且借助小程序的生态流量入口、原生能力等,使得企业更听倾向于先开发小程序。
首先最重要的是代码规范问题,由于每个团队每个项目或多或少都有一些代码规范,改动某些规范的代价甚至高于重新开发相同的功能(因为要考虑历史包袱)。但是模块化、组件化、按需注入、TreeSharking
是良好代码规范的必要条件!
模块化的要求:页面逻辑、组件设计要考虑模块化,要因地制宜的考虑问题。
模块化的注意事项
有人喜欢面向对象的编程思想,于是定义接口时使用class
js/**
* api.ts
* 这种class写法,不支持treeSharking
* 只用引用其中一个对象,整个文件都会打包进去
*/
class homeApi {
static getBanner = () => "BANNER+++++",
static getList = () => 'LIST+++++'
}
class mapApi {
static getCity() {
console.log('GET-CITY----')
// ...
}
// ...
}
// 如果确实需要封装为class,建议尽量一个class对应一个文件。
在工具方法、NPM包上也喜欢class或Object
jsconst math = {
add,
minus
}
// 不支持treeSharking
export default math;
export const envUtil = {
codeEnv: XXX
// 注意如果直接使用 envUtil.isWXMP,会使得Taro的条件编译失效
isWXMP: process.env.TARO_ENV === 'weapp',
isAlipay: process.env.TARO_ENV === 'alipay',
}
// npm 包
epxort const urlUtil = {
getParams: ...,
getDomain: ...,
// ...
}
export const stringUtil = {
// ...
}
export const functionUtil {
// ...
}
// 不建议上面的大工具方法导出
// 推荐如下的小工具方法导出,这样能更充分的treeSharking
export const getParams = ...
export const getDomain = ...
// 更不建议如下写法!
// 不只是不能treeSharking问题,还会导致垃圾回收变慢
/* export default {
urlUtil,
stringUtil,
functionUtil,
} */
上述的这些写法看似遵循模块化的思想,但实际上有一连串的问题
这些写法会造成,代码无法 TreeShark
,尤其是 Object
和Class
,相互嵌套引用,使得首屏加载的包过大,尤其是在小程序上非常明显。
当大量公共方法、组件、NPM包采用改该写法时,由于包体积的限制,我们可以通过拆离主包页面、首页重定向、optimizeMainPackage优化主包体积来实现,但是这样方法多是治标不治本,甚至是饮鸩止渴。
首先,拆离主包页面,这个还好,确实提高了首屏加载体验,再通过预加载配置,页面切换也还算流畅,唯一影响的是首屏加载的不是首页,这就需要加载两个分包。
其次,首页重定向,这个是治标不治本,要加载两个包才能看到首屏内容。
最后optimizeMainPackage优化主包体积,将一些内容打入分包,但是如果有多个分包使用,会分别进入多个分包,使得总包体积被成倍放大。
包体积越大 ---> 代码拉取和注入的时间变长(在弱网环境下尤其明显) ---> JS解析所需时间越长(正相关性)---> 首屏加载变长
在大的项目中,有成千上万的接口,如果参考上面的规范,大量接口聚合为一个对象。参考V8引擎的垃圾回收机制,这些大对象很快新生区晋升为老生区(新生区中的对象经过两次垃圾回收还没有销毁的对象会进入老生区),大量的大对象会占据在老生区,这些大对象很难被会回收(除非大对象上的每个属性都没有引用了,才会回收),这就会造成 老生区每次垃圾回收变得越来越慢,即老生区全停顿时间变长,页面交互(跳转、点击等)变慢,表现出来卡顿。
可以微信小程序开发者工具的Performance monitor
工具可以观察内存是否在缓慢增长
如果大量使用这些Object
、Class
的模块化规范 ---> 使得主包变大 ---> 进而使得IDE预览和刷新(改动代码后)等待的时间显著变成。
如果只是等待时间长一点还能接受,但是对于大项目(或者随着业务迭代)主包很快超出4M
(真机调试主包大小), 如果还想真机调试就只能在开发模式下压缩代码了,这样带来的问题是,对于模拟器正常真机异常这种情况下的问题,定位问题变得刀耕火种!
如何解决上述问题呢?
正确的模块化姿势
// math.ts 支持treeSharking export function add(a: number, b: number) { console.log('add+++++') return a + b } export function minus(a: number, b: number) { console.log('minus+++++') return a - b } // 支持treeSharking import * as Math from '../utils/math' // math2.ts 支持treeSharking function add(a: number, b: number) { console.log('add+++++') return a + b } function minus(a: number, b: number) { console.log('minus+++++') return a - b } export { add, minus } // 支持treeSharking import {add} from '../utils/math' import * as Math from '../utils/math' import * as Math2 from '../utils/math2'
上述代码都会触发tenser
的treeSharking
。此外wepack4
还有一个作用域提升的功能(减少一点点包体积)。
Scope Hoisting
(作用域提升)webpack4
的production
模式下默认会使用一个ModuleConcatenationPlugin
的插件,它就是作用域提升。
举例来说,A.js
引入了B.js
的fb
变量, 而这个变量fb
仅被A.js
引入,那么fb
这个变量或函数等于写在A.js
里面, 这样减少了引用链条, 所以降低了包体积
Taro
的条件编译其实比较简单,借助tenser
消除deadcode
js/** 源码 */
if (process.env.TARO_ENV === 'weapp') {
require('path/to/weapp/name')
} else if (process.env.TARO_ENV === 'h5') {
require('path/to/h5/name')
}
/* 假设在小程序上编译,编译后*/
/** 编译后(微信小程序)*/
if (true) {
require('path/to/weapp/name')
}
if (false) {
require('path/to/h5/name')
}
// 最后经过tenser压缩后,只剩下
require('path/to/weapp/name')
但是千万不要对process.env.TARO_ENV
另取别名处理,如下操作,都会导致条件编译失效
tsexport const envUtil = {
codeEnv: XXX
// 注意如果直接使用 envUtil.isWXMP,会使得Taro的条件编译失效
isWXMP: process.env.TARO_ENV === 'weapp',
isAlipay: process.env.TARO_ENV === 'alipay',
}
function fa() {
switch(process.env.TARO_ENV) { // 这里也会导致条件编译失效
case 'weapp': {
// ...
break;
}
case 'alipay': {
// ...
break;
}
}
}
模版文件及css文件可以通过如下方式进行条件编译
css/* #ifdef weapp */
模板代码
/* #endif */
/* 剔除 alipay 平台 */
/* #ifndef alipay */
模板代码
/* #endif */
taro支持多端文件的条件编译
md├── test.js Test 组件默认的形式,编译到微信小程序、百度小程序和 H5 之外的端使用的版本 ├── test.weapp.js Test 组件的微信小程序版本 优先于test.js ├── test.swan.js Test 组件的百度小程序版本 优先于test.js └── test.h5.js Test 组件的 H5 版本 优先于test.js
我们可以通过jscpd
这个工具来扫描项目中的重复代码, 比如如下命令
jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'
以上命令检测两个代码块至少200个字母且至少20行重复的,然后在分析哪些被打入主包优先处理掉。
适度的减少重复代码,主要收益是可以降低项目的维护迭代成本,此外还有一些其他好处
配置 config/index.js 伪代码如下。
jsconst shouldRemoveLog = process.env.NODE_ENV = 'production' && process.env.isCI;
mini: {
optimization: {
minimizer: [
new TerserWebpackPlugin({
terserOptions: {
compress: {
drop_console: shouldRemoveLog, // 移除console相关
drop_debugger: shouldRemoveLog, // 移除debugger
pure_funcs: !shouldRemoveLog ? [] : [
'console.log',
"console.table",
"console.error",
"console.warn",
"console.info",
] // 如果console.log有副作用,比如console.log(a++), 将保留
}
}
});
]
}
}
我们可以通过在微信小程序后台和支付宝小程序后台配置小程序基础版版本,从而在项目中减少一些api的抹平处理,一定程度上降低包体积
小程序基础库版本与客户端(微信or支付宝)版本有关, 如果目标小程序的基础库版本过高,微信or支付宝在打开小程序前进行拦截,提升用户升级微信or支付宝
升级了小程序基础库版本,我们可以顺带升级一下browserlist配置。
在微信小程序中可以根据基础库版本获取browserlist配置
参考 小程序内置的Polyfill
然而支付宝小程序browserlist配置与小程序的基础库无关,仅与手机系统版本有关,可以参考web开发browserlist配置
如果同时使用taro开发微信和支付宝小程序,则要综合两者的browserlist配置取最低版本配置
对于taro项目需要修改两处地方
babel.config.js
此处会影响Taro框架及项目JS代码的babel处理
package.json
中的browserslist
字段此处影响的是CSS
postcss 对css的打布丁非常粗暴简单, 只使用了autoprefix对css增加前缀,如果你使用了比较新的css如grid,postcss不会做任何事情。 所以css体积的减少主要来自css前缀移除
browserlist的升级效果基本上与要兼容的android最低版本有关(IOS手机版本更新的很快).
以Taro@3
为例package.json中browserlist默认值为
json{
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
]
}
babel.config.ts的默认配置是
js{
ios: '9',
android: '5'
}
这个配置已经很古老的了(几乎需要编译所有es6句法、语法和CSS前缀),而且其实对于移动端只需要配置后两项就行了。如果将配置改为 android 7
、ios 12
chrome >= 52 ios >= 12
从我个人的经验看上述配置已经覆盖了全部机型,会省去大部分es6句法转换和API垫片,使得包体积降低,对于支付宝小程序效果会明显一些(支付宝IDE会进行句法转换,taro编译只增加API垫片,而微信小程序正好相反,它是内置了API垫片,需要tao对es6句法进行转换)。
在taro官网上有明确要求,如果使用taro开发的开发的小程序,构建后需要关闭小程序开发者上的项目设置
browserlist
的调整不会对npm包有影响,因为npm
包是单独构建的,这会导致npm
中有与项目构建后重复的API
垫片,句法转换辅助函数,npm包越多越明显!
如果我们让npm包只提供源码,复用项目中的browserlist配置进行构建,这样整个项目只有一份API垫片和句法转换的辅助函数,极大的减少主包体积。
taro默认会将node_module的内容构建到主包的vendor文件中
那么如何配置呢? 参考官方文档:[解析 node_modules 内的多端文件](解析 node_modules 内的多端文件)
配置示例如下
config/index.js
jsmini: {
webpackChain (chain) {
// Taro 3.3+
chain.resolve.plugin('MultiPlatformPlugin')
.tap(args => {
args[2]["include"] = ['@your-company']
return args
})
}
}
这样配置之后, 仅在主包页面引用的npm的某部分内容才会构建到主包文件中,其他子包页面引用的npm的那部分代码会构建进入子包。
但是对于UI组件库组件库来说,还需要一些处理,印象中有两点调整
scss
or less
不能引用本地图片,需改成外链的形式scss
or less
好像有一些变量或css引入需要调整
- 分包:功能上类似于h5的根据路由按需加载页面
- 分包预下载:在进入小程序某个页面时,由框架自动预下载可能需要的分包,提升进入后续分包页面时的启动速度。
- 分包异步化: 分包之间的相互依赖默认会打入到主包,为了降低主包大小,提升首屏体验,将分包之间的共同依赖构建到一个单独的子包中。
在taro项目中,比较好实现的是 配置分包和配置分包预下载,与原生小程序项目的配置完全相同,风险很低。 分包异步化我没有实践过,因为要写Taro插件,难度和风险比较高。独立分包用的很少,因为它的使用场景是在使用多种框架开发的小程序上,而且支付宝小程序不支持。
微信文档 按需注入和用时注入
taro项目可以配置按需注入,方法很简单在app.config.ts
中增加一行代码"lazyCodeLoading": "requiredComponents",
即可, 比微信小程序原生项目配置要简单,后者配置后需要检查每个页面的usingComponents
字段,而taro会自动生成usingComponents
,不需要检查。改动很简单,但实测收益也很小很小,仅在分包包含的页面很多很复杂的情况下,能测出不明显的首屏渲染耗时减少。
支付宝小程序没有按需注入的功能,增加这个配置对支付宝小程序也无影响。
用时注入
这个对于taro项目来说不太适用,一方法配置用时注入需要一个组件一个组件的改造 投入产出比很低,另一方面支付宝小程序不支持,可能需要一些适配工作。
我曾调研过通过将mobx从4升级到5及以上,用Proxy代替Object.defineProperty,收益如下
在mobx@4中的开发中,为了图方便我们经常对大对象直接赋值,会导致setData的数据量和频次增加,从而影响小程序性能。如果使用mobx@5就不会有这样的问题, 但是升级到mobx@5及以上代价也很大
参考:Mobx踩坑&性能优化
mobx的store是Class写法, 需要避免各个页面的Store相互引用,应定义一个全局的 store来存储个页面(各业务)公共的数据依赖。
本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!