WePY 在小程序性能调优上作出的探究

性能调优是一个亘古不变的话题,不管是在传统H5上仍是小程序中。由于实现机制不一样,可能致使传统H5中的某些优化方式在小程序上并不适用。所以必须另开辟蹊径找出适合小程序的调估方式。javascript

预先加载

原理

传统H5中也能够经过预加载来提高用户体验,但在小程序中作到这一点其实是能够更简单方便却又更容易被忽视的。html

传统H5在启动时,page1.html 只会加载 page1.html 的页面与逻辑代码,当page1.html 跳转至 page2.html 时,page1 全部的 Javascript 数据将会从内存中消失。page1 与 page2 之间的数据通讯只能经过 URL 参数传递或者浏览器的 cookie,localStorge 存储处理。vue

小程序在启动时,会直接加载全部页面逻辑代码进内存,即使 page2 可能都不会被使用。在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至能够直接访问 page1 中的数据。java

最简单的验证方式就是在 page1 中加入一个 setInterval(function () {console.log('exist')}, 1000)。传统H5中跳转后定时器会自动消失,小程序中跳转后定时器仍然工做。react

小程序的这种机制差别正好能够更好的实现预加载。一般状况下,咱们习惯将数据拉取写在 onLoad 事件中。可是小程序的 page1 跳转到 page2,到 page2 的 onLoad 是存在一个 300ms ~ 400ms 的延时的。以下图:git

图片描述github

由于小程序的特性,彻底能够在 page1 中预先拿取数据,而后在 page2 中直接使用数据,这样就能够避开 redirecting 的 300ms ~ 400ms了。以下图:web

图片描述json

试验

在官方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);
}

在通过十次刷新运行测试后得出如下结果:

worse(ms) best(ms) digest(ms)
8540 24 23
7784 22 25
7884 23 25
8317 22 25
7968 28 26
7939 21 23
7853 22 23
8053 25 23
8007 24 29
8168 25 24

实现一样的逻辑,性能数据却相差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

获得的结果以下:

图片描述

从结果来看,对于一个 16.7 KB 的数据深比较是彻底不足以产生性能问题的。那 AngularJS 1.x 脏检查的性能问题是怎么出现的呢?

AngularJS 1.x 中没有组件的概念,页面数据就位于 controller 的 &dollar;scope 当中。每一次脏检查都是从 &dollar;rootScope 开始,随后遍历至全部子 &dollar;scope。参考这里 angular.js:L1081。对于一个大型的单页应用来讲,全部 &dollar;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";
  }
}

结束语:小程序还存在不少值得开发者去探索优化的地方,欢迎你们与我探讨交流开发心得。若本文存在不许确的地方,欢迎批评指正。

相关文章
相关标签/搜索