小程序组件化框架 WePY 在性能调优上做出的探究
作者:龚澄
导语
性能调优是一个亘古不变的话题,无论是在传统H5上还是小程序中。因为实现机制不同,可能导致传统H5中的某些优化方式在小程序上并不适用。因此必须另开辟蹊径找出适合小程序的调估方式。
本文旨在介绍两点在小程序开发过程当中碰到的一些性能问题以及 WePY 的一些优化方案。
小程序组件化框架 WePY 介绍请阅读:《打造“微信小程序”组件化开发框架》
预先加载
原理
传统H5中也可以通过预加载来提升用户体验,但在小程序中做到这一点实际上是可以更简单方便却又更容易被忽视的。
传统H5在启动时,page1.html 只会加载 page1.html 的页面与逻辑代码,当page1.html 跳转至 page2.html 时,page1 所有的 Javascript 数据将会从内存中消失。page1 与 page2 之间的数据通信只能通过 URL 参数传递或者浏览器的 cookie,localStorge 存储处理。
小程序在启动时,会直接加载所有页面逻辑代码进内存,即便 page2 可能都不会被使用。在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至可以直接访问 page1 中的数据。
最简单的验证方式就是在 page1 中加入一个 setInterval(function () {console.log('exist')}, 1000)
。传统H5中跳转后定时器会自动消失,小程序中跳转后定时器仍然工作。
小程序的这种机制差异正好可以更好的实现预加载。通常情况下,我们习惯将数据拉取写在 onLoad 事件中。但是小程序的 page1 跳转到 page2,到 page2 的 onLoad 是存在一个 300ms ~ 400ms 的延时的。如下图:
因为小程序的特性,完全可以在 page1 中预先拿取数据,然后在 page2 中直接使用数据,这样就可以避开 redirecting 的 300ms ~ 400ms了。如下图:
试验
在官方demo中加入两个页面:page1,page2
// page1.js 点击事件中记录开始时间
bindTap: function () {
wx.startTime = +new Date();
wx.navigateTo({
url: '../page2/page2'
});
}
// page2.js 中假设从服务器拉取数据需要500ms
fetchData: function (cb) {
setTimeout(function () {
cb({a:1});
}, 500);
},
onLoad: function () {
wx.endTime = +new Date();
this.fetchData(function () {
wx.endFetch = +new Date();
console.log('page1 redirect start -> page2 onload invoke -> fetch data complete: ' + (wx.endTime - wx.startTime) + 'ms - ' + (wx.endFetch - wx.endTime) + 'ms');
});
}
重试10次,得到的结果如下:
优化
对于上述问题,WePY 中封装了两种概念去解决:
- 预加载数据
用于 page1 主动传递数据给 page2,比如 page2 需要加载一份耗时很长的数据。我可以在 page1 闲时先加载好,进入 page2 时直接就可以使用。 - 预查询数据
用于避免于 redirecting 延时,在跳转时调用 page2 预查询。
扩展了生命周期,添加了onPrefetch事件,会在 redirect 之时被主动调用。同时给onLoad事件添加了一个参数,用于接收预加载或者是预查询的数据:
// params
// data.from: 来源页面,page1
// data.prefetch: 预查询数据
// data.preload: 预加载数据
onLoad (params, data) {}
预加载数据示例:
// page1.wpy 预先加载 page2 需要的数据。
methods: {
tap () {
this.$redirect('./page2');
}
},
onLoad () {
setTimeout(() => {
this.$preload('list', api.getBigList())
}, 3000)
}
// page2.wpy 直接从参数中拿到 page1 中预先加载的数据
onLoad (params, data) {
data.preload.list.then((list) => render(list));
}
预查询数据示例:
// page1.wpy 使用封装的 redirect 方法跳转时,会调用 page2 的 onPrefetch 方法
methods: {
tap () {
this.$redirect('./page2');
}
}
// page2.wpy 直接从参数中拿到 onPrefetch 中返回的数据
onPrefetch () {
return api.getBigList();
}
onLoad (params, data) {
data.prefetch.then((list) => render(list));
}
数据绑定
原理
在针对数据绑定做优化时,需要先了解小程序的运行机制。因为视图层与逻辑层的完全分离,所以二者之间的通信全都依赖于 WeixinJSBridge 实现。如:
- 开发者工具中是基于
window.postMessage
- iOS中基于
window.webkit.messageHandlers.invokeHandler.postMessage
- Android中基于
WeixinJSCore.invokeHandler
因此数据绑定方法this.setData
也如此,频繁的数据绑定就增加了通信的成本。再来看看this.setData
究竟做了哪些事情。基于开发者工具的代码,单步调试大致还原出完整的流程,以下是还原后的代码:
/*
setData 主流程精简还原,并非完整主流程,内有注释
*/
function setData (obj) {
if (typeof(obj) !== 'Object') {
console.log('类型错误'); // 并没有预期中的return;
}
let type = 'appDataChange';
// u.default.emit(e, this.__wxWebviewId__) 代码还原
let e = [type, {
data: {data: list},
options: {timestamp: +new Date()}
},
[0] // this.__wxWebviewId__
}];
// WeixinJSBridge.publish.apply(WeixinJSBridge, e); 代码还原
var datalength = JSON.stringify(e.data).length; // 第一次 JSON.stringify
if (datalength > AppserviceMaxDataSize) { // AppserviceMaxDataSize === 1048576
console.error('已经超过最大长度');
return;
}
if (type === 'appDataChange' || type === 'pageInitData' || type === '__updateAppData') {
// sendMsgToNW({appData: __wxAppData, sdkName: "send_app_data"}) 代码还原
__wxAppData = {
'pages/page1/page1': alldata
}
e = { appData: __wxAppData, sdkName: "send_app_data" }
var postdata = JSON.parse(JSON.stringify(e)); // 第二次 JSON.stringify 第一次 JSON.parse
window.postMessage({
postdata
}, "*");
}
// sendMsgToNW({appData: __wxAppData, sdkName: "send_app_data"}) 代码还原
e = {
eventName: type,
data: e[1],
webviewIds: [0],
sdkName: 'publish'
};
var postdata = JSON.parse(JSON.stringify(e)); // 第三次 JSON.stringify 第二次 JSON.parse
window.postMessage({
postdata
}, "*");
}
setData 运行的流程如下:
从上面代码以及流程图中可以看出,在一次setData({a: 1})
作时,会进行三次 JSON.stringify
,二次JSON.parse
以及两次window.postMessage
操作。并且在第一次window.postMessage
时,并不是单单只处理传递的{a:1}
,而是处理当前页面的所有 data 数据。因此可想而知每次setData
操作的开销是非常大的,只能通过减少数据量,以及减少setData
操作来规避。
与 setData
相近的是 React
的 setState
方法,同样是使用 setState
去更新视图的,可以通过源码 React:L199 看到 setState
的关键代码如下:
function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
setState
的工作流程如下:
可以看出,setState 加入了一个缓冲列队,在同一执行流程中进行多次 setState 之后也不会重复渲染视图,这就是一种很好的优化方式。
实验
为了证实setData
的性能问题,可以写简单的测试例子去测试:
动态绑定1000条数据的列表进行性能测试,这里测试了三种情况:
- 最优绑定:在内存中添加完毕后最后执行
setData
操作。 - 最差绑定:在添加一条记录执行一次
setData
操作。 - 最智能绑定:不管中间进行了什么操作,在运行结束时执行一次脏检查,对需要设置的数据进行
setData
操作。
参考代码如下:
// page1.wxml
<view bindtap="worse">
<text class="user-motto">worse数据绑定测试</text>
</view>
<view bindtap="best">
<text class="user-motto">best数据绑定测试</text>
</view>
<view bindtap="digest">
<text class="user-motto">digest数据绑定测试</text>
</view>
<view class="list">
<view wx:for="{{list}}" wx:for-index="index"wx:for-item="item">
<text>{{item.id}}</text>---<text>{{item.name}}</text>
</view>
</view>
// page1.js
worse: function () {
var start = +new Date();
for (var i = 0; i < 1000; i++) {
this.data.list.push({id: i, name: Math.random()});
this.setData({list: this.data.list});
}
var end = +new Date();
console.log(end - start);
},
best: function () {
var start = +new Date();
for (var i = 0; i < 1000; i++) {
this.data.list.push({id: i, name: Math.random()});
}
this.setData({list: this.data.list});
var end = +new Date();
console.log(end - start);
},
digest: function () {
var start = +new Date();
for (var i = 0; i < 1000; i++) {
this.data.list.push({id: i, name: Math.random()});
}
var data = this.data;
var $data = this.$data;
var readyToSet = {};
for (k in data) {
if (!util.$isEqual(data[k], $data[k])) {
readyToSet[k] = data[k];
$data[k] = util.$copy(data[k], true);
}
}
if (Object.keys(readyToSet).length) {
this.setData(readyToSet);
}
var end = +new Date();
console.log(end - start);
},
onLoad: function () {
this.$data = util.$copy(this.data, true);
}
在经过十次刷新运行测试后得出以下结果:
实现同样的逻辑,性能数据却相差40倍左右。由此可以看出,在开发过程中,一定要避免同一流程内多次 setData 操作。
优化
在开发时,避免在同一流程内多次使用setData
当然是最佳实践。采取人工维护肯定是能够实现的,就好比能用原生 js 能写出比众多框架更高效的性能一样。但当页面逻辑负责起来之后,花很大的精力去维护都不一定能保证每个流程只存在一次setData
,而且可维护性也不高。因此,WePY选择使用脏检查去做数据绑定优化。用户不用再担心在我的流程里,数据被修改了多少次,只会在流程最后做一次脏检查,并且按需执行setData
。
脏检测机制借鉴自AngularJS,多数人一听到脏检查都会觉得是低效率的一种作法,认为使用 Vue.js 中的 getter,setter更高效。其实不然,两种机制都是对同一件事的不同实现方式。各有优劣,取决于使用的人在使用过程中是否正好放大了机制中的劣势面。
WePY 中的 setData
就好比是一个 setter,在每次调用时都会去渲染视图。因此如果再封装一层 getter、setter 就完全没有意义,没有任何优化可言。这也就是为什么一个类 Vue.js 的小程序框架却选择了与之相反的另外一种数据绑定方式。
再回来看脏检查的问题在哪里,从上面实验的代码可以看出,脏检查的性能问题在于每次进行脏检查时,需要遍历所以数据并且作值的深比较,性能取决于遍历以及比较数据的大小。WePY 中深比较是使用的 underscore 的 isEqual 方法。为了验证效率问题,使用不同的比较方法对一个 16.7 KB 的复杂 JSON 数据进行深比较,测试用例请看这里:deep-compare-test-case (https://jsperf.com/deep-compare/4)
得到的结果如下:
从结果来看,对于一个 16.7 KB 的数据深比较是完全不足以产生性能问题的。那 AngularJS 1.x 脏检查的性能问题是怎么出现的呢?
AngularJS 1.x 中没有组件的概念,页面数据就位于 controller 的 $scope 当中。每一次脏检查都是从 $rootScope 开始,随后遍历至所有子 $scope。参考这里 angular.js:L1081。对于一个大型的单页应用来说,所有 $scope 中的数据可能达到了上百甚至上千个都有可能。那时,脏检查的每次遍历就可能真的会成为了性能的瓶颈了。
反观 WePY,使用类似于 Vue.js 的组件化开发,在抛开父子组件双向绑定通信的情况下,组件的脏检查仅针对组件本身的数据进行,一个组件的数据通常不会太多,数据太多时可以细化组件划分的粒度。因此在这种情况下,脏检查并不会导致性能问题。
其实,在很多情况下,框架封装的解决方案都不是性能优化的最优解决方案,使用原生肯定能优化出更快的代码。但它们之所以存在并且有价值,那都是因为它们是在性能、开发效率、可维护性上寻找到一个平衡点,这也是为什么 WePY 选择使用脏检查作为数据绑定的优化。
其它优化
除了以上两点是基于性能上做出的优化以外,WePY 也作出了一系列开发效率上的优化。因为在我之前的文章里都有详细说明,所以在这里就简单列举一下,不做深入探讨。详情可以参看 WePY 文档。
组件化开发
支持组件循环、嵌套,支持组件 Props 传值,组件事件通信等等。
parent.wpy
<child :item.sync="myitem" />
<repeat for="{{list}}" item="item" index="index">
<item :item="item" />
</repeat>
支持丰富的编译器
js 可以选择用 Babel 或者 TypeScript 编译。
wxml 可以选择使用 Pug(原Jade)。
wxss 可以选择使用 Less、Sass、Styus。
支持丰富的插件处理
可以通过配置插件对生成的js进行压缩混淆,压缩图片,压缩 wxml 和 json 已节省空间等等。
支持 ESLint 语法检查
添加一行配置就可以支持 ESLint 语法检查,可以避免低级语法错误以及统一项目代码的风格。
生命周期优化
添加了 onRoute 的生命周期。用于页面跳转后触发。
因为并不存在一个页面跳转事件(onShow 事件可以用作页面跳转事件,但同时也存在负作用,比如按 HOME 键后切回来,或者拉起支付后取消,拉起分享后取消都会触发 onShow 事件)。
支持 Mixin 混合
可以灵活的进行不同组件之间的相同功能的复用。参考 Vue.js 官方文档: 混合
优化事件,支持自定义事件
bindtap="tap" 简写为 @tap="tap",catchtap="tap"简写为@tap.stop="tap"。
对于组件还提供组件自定义事件
<child @myevent.user="someevent" />
优化事件传参
官方版本如下:
<view data-alpha-beta="1" data-alphaBeta="2" bindtap="bindViewTap"> DataSet Test </view>
Page({
bindViewTap:function(event){
event.target.dataset.alphaBeta === 1 // - 会转为驼峰写法
event.target.dataset.alphabeta === 2 // 大写会转为小写
}
})
优化后:
<view @tap="bindViewTap("1", "2")"> DataSet Test </view>
methods: {
bindViewTap(p1, p2, event) {
p1 === "1";
p2 === "2";
}
}
结语
小程序还存在很多值得开发者去探索优化的地方,欢迎大家与我探讨交流开发心得。若本文存在不准确的地方,欢迎批评指正。
更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!
小程序组件化框架 WePY 在性能调优上做出的探究的更多相关文章
- 小程序组件化开发框架---wepy 项目创建
wepy是一个优秀的微信小程序组件化框架,突破了小程序的限制,支持了npm包加载以及组件化方案.这里就以我个人的经历讲下怎么创建wepy项目. 1.首先 在桌面(自己选定目录下)新建一个文件夹,注意需 ...
- 微信小程序组件化开发框架WePY
wepy-CLI 安装 npm install -g wepy-cli wepy init standard my-project https://github.com/Tencent/wepy 特性 ...
- WePY | 小程序组件化开发框架
资源连接: WePY | 小程序组件化开发框架 WePYAWESOME 微信小程序wepy开发资源汇总 文档 GITHUB weui WebStorm/PhpStorm 配置识别 *.wpy 文件代码 ...
- 微信小程序组件化实践
Do Not Repeat Yourself 如何提高代码质量,方法有许多:抽象.模块.组件化,我认为它们的中心点都是--Do Not Repeat Yourself. 小程序组件化 我们先看看小程序 ...
- 【腾讯Bugly干货分享】打造“微信小程序”组件化开发框架
本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/2nQzsuqq7Avgs8wsRizUhw 作者:Gc ...
- 程序员必须掌握的性能调优 X Y Z
热评博文:<如何设计出优美的Web API?>,现阅读量超 2500,小伙伴们不要错过哦! 2003 ~ 2008 年,这五年老兵哥我在通信行业做实习生和开发岗,主要用 C / C++ / ...
- 用Vue.js开发微信小程序:开源框架mpvue解析
前言 mpvue 是一款使用 Vue.js 开发微信小程序的前端框架.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小程序提供了代码复用的能力.如果想将 H5 项目改造为小程 ...
- Atlas-手淘组件化框架的前世今生和未来的路
今天手淘技术团队宣布正式开源它们的容器框架Atlas,项目地址: https://github.com/alibaba/atlas 同时他们还推出了项目官网,上线了技术文档: http://atlas ...
- Android组件化框架设计与实践
在目前移动互联网时代,每个 APP 就是流量入口,与过去 PC Web 浏览器时代不同的是,APP 的体验与迭代速度影响着用户的粘性,这同时也对从事移动开发人员提出更高要求,进而移动端框架也层出不穷. ...
随机推荐
- JS 冷知识,运行机制
数组取最小.最大值 var a=[1,2,3,5]; alert(Math.max.apply(null, a));//最大值 alert(Math.min.apply(null, a));//最小值 ...
- tensorflow--交叉熵
学而不思则罔,思而不学则怠. 最近在看<TensorFlow 实战Google深度学习框架第二版>这本书.从头开始学习深度学习,对于细节方面进行探究.相当于重新拾起来这门”手艺“. 这篇随 ...
- ASP.NET Core + Vue.js 开发
1.新建 项目文件夹 pro,在 VS CODE 打开终端,输入dotnet new mvc 命令,新建asp.net core项目. 2.在Startup.cs添加webpack的引用与配置 usi ...
- 七 Git版本控制
把环境准备 主机名 node1 ip地址10.0.0.11 node2 10.0.0.12 node3 ...
- openv
https://www.cnblogs.com/xishuai/p/centos-openvpn.html https://www.cnblogs.com/biaopei/category/11029 ...
- linux常用命令及使用技巧(二)
ls显示指定工作目录下的内容,同windows中的dir命令 pwd命令显示当前工作目录 date命令,显示或修改系统时间与日期 passwd命令,设置用户密码 su命令改变用户身份 clear命令, ...
- skopeo---github简单记录
新的redhat 8已默认不集成docker,而是使用podman了. podman和skopeo和buildah,cri-o一起,组成了新一代的容器四大天王. 花时时间了解下,是值得的. skope ...
- git20181122
git 在线编辑器 http://www.mdeditor.com/git add commit diff log status 代码撤消 # git https://github.com/gyz41 ...
- cmd命令中运行pytest命令导入模块报错解决方法
报错截图 ImportError while loading conftest 'E:\python\HuaFansApi\test_case\conftest.py'. test_case\conf ...
- Kubernetes系列之Helm介绍篇
本次系列使用的所需部署包版本都使用的目前最新的或最新稳定版,安装包地址请到公众号内回复[K8s实战]获取 介绍 Helm 是 Deis 开发的一个用于 Kubernetes 应用的包管理工具,主要用来 ...