Introduction:
Performance tuning is an immutable topic, both on traditional H5 and in applets. Because of the different implementation mechanism, some optimization methods in traditional H5 may not be suitable for small programs. Therefore, we must find another way to find a suitable way for small programs.
preloaded
This section is based on research based on anniexliu’s article, “optimizing small program performance — improving page loading speed.”
The principle of
Traditional H5 can also improve the user experience through preloading, but doing so in small programs is actually easier and easier to overlook.
When traditional H5 starts, page1. HTML will only load the page and logical code of page1. When page1. Data communication between Page1 and Page2 can only be passed through URL parameters or browser cookies, localStorge storage processing.
When the applet starts, it loads all the page logic directly into memory, even though page2 may not be used. When Page1 jumps to Page2, the Javascript data of Page1’s logical code does not disappear from memory. Page2 can even access the data in Page1 directly.
The easiest way to verify this is to add a setInterval(function () {console.log(‘exist’)}, 1000) to page1. In traditional H5, the timer will disappear automatically after the jump, while in small program, the timer still works after the jump.
This mechanism difference of small program just can realize better preloading. In general, we are used to writing pull data in the onLoad event. But small program page1 jumps to page2, to page2 onLoad is a 300ms ~ 400ms delay. The diagram below:
Due to the nature of a minor program, data could have been taken in advance in PAGe1 and used directly in Page2. Thus, the 300ms ~ 400ms of reDIRECTING can be avoided. The diagram below:
test
Add two pages in the official demo: Page1 and Page2
BindTap: function () {wx.startTime = +new Date(); wx.navigateTo({ url: '.. /page2/page2' }); 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'); }); }Copy the code
Retry 10 times and get the following result:
To optimize the
WePY encapsulates two concepts to solve these problems:
- Preloaded data
Page1 is used to actively pass data to Page2. For example, Page2 needs to load a data that takes a long time. I can load it when PAGe1 is idle and use it when I enter Page2.
- Pre-query data
Used to avoid delay in redirecting. Call page2 pre-query on a directing.
Extended the lifecycle by adding onPrefetch events that are called actively at redirect time. Add a parameter to the onLoad event to receive pre-loaded or pre-queried data:
// params // data. From: source page, page1 // data. Preload: preload data. // data.Copy the code
Examples of preloaded data:
// page1.wpy preloads the data needed for page2. methods: { tap () { this.$redirect('./page2'); } }, onLoad () { setTimeout(() => { this.$preload('list', api.getBigList()) }, If (params, data) {data.preload.list. Then ((list) => render(list)); if ((list) => render(list)); }Copy the code
Example of pre-query data:
// page1.wpy calls page2 onPrefetch methods: {tap () {this.$redirect('./page2'); Return api.getBigList(); return api.getBigList(); } onLoad (params, data) { data.prefetch.then((list) => render(list)); } cCopy the code
Data binding
The principle of
When optimizing for data binding, you need to understand how applets work. Because the view layer is completely separated from the logical layer, the communication between them all depends on WeixinJSBridge. Such as:
-
The developer tools are based on window.postMessage
-
Based on the window in IOS. Its. MessageHandlers. InvokeHandler. PostMessage
-
Based on WeixinJSCore in Android. InvokeHandler
So does the data binding method this.setData, and frequent data binding increases the cost of communication. Let’s take a look at what this.setData does. Based on the code of the developer tools, the single-step debugging roughly restores the complete process. Here is the restored code:
*/ function setData (obj) {if (typeof(obj)! == 'Object') {console.log(' type error '); // Return is not expected; } let type = 'appDataChange'; Let e = [type, {data: {data: list}, options: {timestamp: +new Date()} }, [0] // this.__wxWebviewId__ }]; // WeixinJSBridge.publish.apply(WeixinJSBridge, e); Var datalEngth = json.stringify (e.datata).length; JSON. Stringify if (datalEngth > AppserviceMaxDataSize) {// AppserviceMaxDataSize === 1048576 Console. error(' The maximum length has been exceeded '); return; } if (type === 'appDataChange' || type === 'pageInitData' || type === '__updateAppData') { // sendMsgToNW({appData: __wxAppData, sdkName: "send_app_data"}) code restore __wxAppData = {'pages/page1/page1': allData} e = {appData: __wxAppData, sdkName: "send_app_data" } var postdata = JSON.parse(JSON.stringify(e)); 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)); Parse window.postMessage({postdata}, "*"); }Copy the code
The flow of setData is as follows:
As you can see from the above code and flow chart, three json.stringify, two json.parse and two window.postmessage operations are performed on a single setData({a: 1}) operation. And the first time window.postMessage is processed, instead of just passing {a:1}, all data on the current page is processed. Therefore, it is conceivable that the overhead of each setData operation is very large, which can only be avoided by reducing the amount of data and setData operations.
React:L199 Use setState to update a view. React:L199 Use setState to update a view.
function enqueueUpdate(component) { ensureInjected(); if (! batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); }Copy the code
The workflow of setState is as follows:
As you can see, setState adds a buffer queue, and does not repeat the rendering of the view after multiple setstates in the same execution flow, which is a good way to optimize.
The experiment
To verify the performance problems of setData, a simple test example can be written:
Dynamically binding a list of 1000 pieces of data for performance testing, three cases were tested:
-
Optimal binding: The setData operation is performed last after the addition in memory.
-
Worst binding: Perform a setData operation after adding a record.
-
Most intelligent binding: No matter what happens in the middle, a dirty check is performed at the end of the run and a setData operation is performed on the data that needs to be set.
The reference code is as follows:
WXML <view bindtap="worse"> <text class="user-motto"> </text> <view > <view bindtap="best"> <text </ motto > <view > <view bindtap="digest"> </ motto > </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); }Copy the code
After running the test ten times, the following results were obtained:
Implement the same logic, but the performance data is about 40 times different. It can be seen that multiple setData operations within the same process must be avoided in the development process.
To optimize the
It is of course a best practice to avoid using setData more than once in the same process during development. Manual maintenance is certainly possible, just as it is possible to write more efficient performance with native JS than many frameworks. But when the page logic is in charge, spending a lot of effort to maintain may not guarantee that setData exists only once per process, and the maintainability is not high. Therefore, WePY chooses to use dirty checking for data binding optimization. The user no longer has to worry about how many times the data has been modified in my process, only doing a dirty check at the end of the process and executing setData as needed.
The dirty detection mechanism is borrowed from AngularJS, and most people think it is inefficient to use getters and setters in Vue. No, both mechanisms are different ways of doing the same thing. Each has its pros and cons, depending on whether the person using it is amplifying the downside of the mechanic.
SetData in WePY is like a setter that renders the view every time it is called. So it makes no sense to wrap another layer of getters, setters, there’s no optimization. This is why a vue.js-class applet framework has chosen the opposite approach to data binding.
As you can see from the code above, the performance problem with dirty checks is that each time you perform a dirty check, you need to iterate over all data and do a deep comparison of values. Performance depends on the size of the walk and the comparison data. Deep comparisons in WePY use the isEqual method of underscore. To verify the efficiency problem, use different comparison methods to make a deep comparison of a complex JSON data of 16.7 KB, as shown in the following test case: deep-compare-test-case
The results are as follows:
As a result, a 16.7KB data depth comparison is not at all sufficient to cause performance problems. So how did AngularJS 1.x dirty check have a performance problem?
There is no component concept in AngularJS 1.x; the page data is in the controller’s \$scope. Each dirty check starts with \$rootScope and is then traversed through all children \$scope. See angular.js:L1081 here. For a large, single-page application, all the data in \$scope may be in the hundreds or even thousands. At that point, each iteration of the dirty check could really become a performance bottleneck.
In contrast, WePY uses component-based development similar to vue.js. In the case of abandoning the two-way binding communication between parent and child components, the component dirty check is only carried out for the data of the component itself. The data of a component is usually not too much, and when there is too much data, the granularity of component division can be refined. So in this case, dirty checking does not cause performance problems.
In fact, in many cases, the solution encapsulated by the framework is not the best solution for performance optimization, and using native will definitely result in faster code optimization. But they exist and are valuable because they strike a balance between performance, development efficiency, and maintainability, which is why WePY chose to use dirty checking as an optimization for data binding.
Other optimization
In addition to these two performance-based optimizations, WePY also makes a number of development efficiency optimizations. Because I have detailed explanation in the previous article, so here is a simple list, not to do in-depth discussion. See the WePY documentation for details.
Componentized development
Support component loop, nesting, support component Props value, component event communication, and so on.
parent.wpy
<child :item.sync="myitem" />
<repeat for="{{list}}" item="item" index="index">
<item :item="item" />
</repeat>
Copy the code
Rich compiler support
Js can be compiled in either Babel or TypeScript.
WXML can optionally use Pug(formerly Jade).
WXSS can use Less, Sass, or Styus.
Support for rich plug-in processing
You can configure plug-ins to compress and obfuscate generated JS, compress images, compress WXML and JSON to save space, and so on.
ESLint syntax checking is supported
A single line of configuration can be added to support ESLint syntax checking, avoiding low-level syntax errors and unifying the style of project code.
Life cycle optimization
Added the onRoute lifecycle. Trigger after page hopping. Because there is no page jump event (the onShow event can be used as a page jump event, but it also has negative effects, such as cutting back after pressing the HOME button, or canceling after pulling the pay button, or canceling after pulling the share button will trigger the onShow event).
Support Mixin mixing
Flexible reuse of the same function between different components. See vue.js official documentation: Blending
Optimized events to support custom events
Bindtap =”tap” @tap=”tap”, catchtap=”tap” @tap.stop=”tap”
Component custom events are also provided for components
<child @myevent.user="someevent" />
Copy the code
Optimized event parameter passing
The official version is as follows:
<view data-alpha-beta="1" data-alphaBeta="2" bindtap="bindViewTap"> DataSet Test </view> Page({ {bindViewTap: function (event). The event target. The dataset. AlphaBeta = = = 1 / / - turn hump writing event. The target. The dataset, alphaBeta = = = 2 / / Uppercase will be converted to lowercase}})Copy the code
After the optimization:
<view @tap="bindViewTap("1", "2")"> DataSet Test </view> methods: { bindViewTap(p1, p2, event) { p1 === "1"; p2 === "2"; }}Copy the code
conclusion
Small procedures still exist a lot of developers to explore the optimization of the place, welcome to discuss with me to exchange development experience. If there are inaccuracies in this article, you are welcome to criticize and correct them.