Small program rendering principle

Interface rendering under two threads, the logic layer and rendering layer of the small program are separate two threads. In the rendering layer, the host environment will convert WXML into the corresponding JS objects. When the data changes in the logical layer, we need to transfer the data from the logical layer to the rendering layer through the setData method provided by the host environment. After comparing the differences before and after, the differences are applied to the original Dom tree to render the correct UI interface.

It can be concluded that the time of page initialization roughly consists of the initial data communication time of the page and the initial rendering time. Among them, the time index data of data communication starts from the logical layer to organize the time when data is completely received to the view layer. The transmission time is generally positively correlated with the amount of data, and the transmission time will increase significantly if the data is too large. Therefore, reducing the amount of data transmission is an effective way to reduce the data transmission time.

Key factors affecting performance

  1. Go setData frequently

    If setData is removed very frequently (in milliseconds), it results in two consequences:

    • Android users will feel stuck when sliding, operation feedback delay is serious, becauseJSThe thread has been compiling and executing the render, failing to deliver the user action events to the logical layer and the logical layer failing to deliver the action processing results to the view layer in time;
    • There is a delay in rendering due toWebViewJSThreads are always in a busy state, the communication time from the logical layer to the page layer increases, the data message received by the view layer has passed several hundred milliseconds since the time of sending, and the rendering result is not real-time;
  2. Every timesetDataBoth deliver a lot of new data

    From the underlying implementation of setData, we can see that our data transfer is actually a process of evaluateJavascript script. When the amount of data is too large, the compilation and execution time of the script will increase and the WebView JS thread will be occupied.

  3. Background status pagesetData

    When the page enters the background state (invisible to the user), it should not continue to setData, the rendering of the background state page is not felt by the user, and the background state page to setData will also preempt the execution of the front page.

  4. indataTo place a lot of data unrelated to interface rendering

Optimization scheme

1. ReducesetdataThe amount of data

If the data does not affect the rendering layer, do not put it in setData

2. MergersetdataTo reduce the frequency of communication

To avoid calling setData too often, consider combining multiple setData calls into a single setData call

// Don't call setData too often
this.setData({ a: 1 })
this.setData({ b: 2 })
// Most of the time can be optimized to
this.setData({ a: 1.b: 2 })
Copy the code

3. Remove unnecessary event binding (WXMLIn thebindandcatch), thus reducing the amount and frequency of communication

4. Avoid nodesdataPrevents large data in the prefix property of

5. Partial update of the list

In a list, there are n data, using the way of pull-up loading more, if this time want to like a certain data operation, but also timely see the effect of the like.

  • Can be usedsetDataGlobal refresh, after the completion of the like, get data again, global re-render, so do a bit is: convenient, fast! The disadvantage is that the user experience is extremely poor, and there will be a blank period for re-rendering after the user has brushed more than 100 data.
  • You can also use a partial refresh that will be likedidYou pass it, you know what it is, you retrieve it, you look for itidIs the subscript of the data (indexWill not change), withsetDataDo a partial refresh, which can significantly improve rendering speed.
this.setData({
    list[index]=newList[index]
})
Copy the code

6. Data not related to interface rendering should not be placed indata, you can consider setting it inpageObject under other fields

Page({
  onShow: function() {
    // Do not set data that is not used in interface rendering, and put data that is not relevant to the interface outside of data
    this.setData({
      myData: {
        a: 'This string is used in WXML'.b: 'This string is not used in WXML, and it is long ………………………… '}})// Can be optimized to
    this.setData({
      'myData.a': 'This string is used in WXML'
    })
    this._myData = {
      b: 'This string is not used in WXML, and it is long ………………………… '}}})Copy the code

7. Prevent background pagesjsPreempted resources

The applet may have n pages, all of which, while having their own WebView (rendering layer), share the same JS runtime environment. That is, when you jump to another page (let’s say PAGE B), the timer and other JS operations on this page (let’s say page A) are still going on, and will not be destroyed, and will preempt resources on page B.

8. Use cautiononPageScroll

The pageScoll event, which is also a communication, is a communication from the WebView layer to the JS logic layer. This communication is expensive, and given the frequency with which the event is called, the performance of callback functions with complex setData will be poor.

9. Use applets whenever possible

Updates to custom components occur only within the component, independent of the rest of the page, and each component will have its own logical space. Each component has its own independent data, called setData.

Case analysis

Checkpoint: How often you gosetData

Check result: None

Checkpoint: Every timesetDataBoth deliver a lot of new data

Check result: None

Checkpoint: background state pagesetData

Check result: Yes

Cause: Because the modification operation is in the search page, after the user clicks, it will immediately return to the upper page for data display, so setData is carried out in the background page to improve the speed of page hopping.

Problem code:

// pages/line/searchResult/searchResult.jsshowSearchDetail(e){ ... Omit codelet prevpage = this.getPrevPage()
        prevpage.setData({
          isInSearch: true,
          showResult,
          keyWord:res.routeName
        })
}
Copy the code

Checkpoint: atdataTo place a lot of data unrelated to interface rendering

Check result: Yes

Causes: Because the current requested interface for querying line information GET/ API /Route/List/{cityID}/{pagesize}/{pageno} does not support paging requests and will return all data at one time, in the previous scheme, in order to reduce the network traffic generated by requests, All of the data is temporarily stored at once in an array on the page (which stores about 600 objects), and portions of the data are displayed as needed.

Problem code:

getLinesInformation(cityID) {
  return new Promise((resolve, reject) = > {
    smartProxy.getRequest(`/Route/List/${cityID}/ 10/0 `)
      .then(res= > {
        this.data.lineArray = res
        if (this.data.lineArray)
          resolve()
        else reject()
      })
  })
},
Copy the code

Solution:

  • Method 1: Trade traffic for performance Instead of temporarily storing information for all lines, the API is requested repeatedly each time a paging request occurs, updating the array of arrays required for page presentation.

    Disadvantages: Repeated requests to the API for the same data waste traffic

    Optimization effect:

    It takes 500ms for the first jump to search the page, 90ms for each subsequent jump to search the page, and 400ms for the average drop-down page load

  • Method 2: Improve the storage scheme. When the data returned by the line API is requested, it is not placed in the data field, but is set to be stored in other fields of the Page object.

    Advantages: Reduces page load and optimizes performance

    Code implementation:

    getLinesInformation(cityID) {
      return new Promise((resolve, reject) = > {
        smartProxy.getRequest(`/Route/List/${cityID}/ 10/0 `)
          .then(res= > {
            // Use the lineArray fields to store the requested data
            this.lineArray = res
            if (this.lineArray)
              resolve()
            else reject()
          })
      })
    },
    Copy the code

Checkpoint: Improper useonPageScroll

Check result: Yes

Cause: The original intention is to realize that users can return to the last browsing position when they return to the line display home page after viewing the line search results, so the onPageScroll event is used to obtain the ScrollTop, and then store it. However, if the onPageScoll event is obtained, the storage will be triggered every time the mixing, which seriously affects the page effect.

Problem code:

onPageScroll: function (e) {
    // execute while the page is scrolling
    // console.log(e);
    if(e.scrollTop ! =0&&!this.data.isInSearch && !this.data.keyWord) {
      // Set the cache
      wx.setStorage({
        key: 'lineSearchScrollTop'.// Cache slide distance, and the current page ID
        data: e.scrollTop
      })
    }
},
Copy the code

Solution:

  • Directly throughwx.createSelectorQuery().selectViewport().scrollOffsetGets the scrollbar height and is called only when the user clicks the search box to jump to the search page, reducedonPageScrollThe impact of events on page performance
// Get the scrollbar height
  getScrollTop () {
    let that = this
    return new Promise((resolve, rej) = > {
      wx.createSelectorQuery().selectViewport().scrollOffset(function (res) {
        that.setData({
          scrollTop: res.scrollTop
        })
        resolve()
      }).exec()
    })
  }
Copy the code

Page life cycle

Before determining performance metrics, it is necessary to take a look at the life cycle of the applet page.

In the parameters of each Page registration function Page(), there are lifecycle methods: onLoad, onShow, onReady, onHide, onUnload.

The first lifecycle callback that a page fires is onLoad, which fires when the page loads and takes the page’s Query argument only once per page;

Next comes onShow, which listens for the display of the page and, unlike onLoad, also triggers this life cycle if the page is hidden and then displayed again (for example, after going to the next page).

After onShow is triggered, the logical layer sends initialization data to the rendering layer. After the rendering layer completes the first rendering, the logical layer is notified to trigger the onReady life cycle, which is only once per page.

NavigateTo, wx. NavigateTo, wx. NavigateTo, wx. NavigateTo, wx.

OnUnload is triggered when a page is unloaded, such as wx.redirectTo or wx.navigateBack to another page.

The whole cycle

Open the page

First, the previous page is hidden, and the components of the new page need to be initialized before the next page can be loaded. After rendering the page for the first time, the component’s ready is triggered, and finally the page’s onReady is triggered, as shown below:

Life cycle order when pageB is opened from PageA

Off-page situations

When you leave the current page, you first trigger onUnload for the current page, followed by detached components from the node tree. Finally, the previous page is displayed, triggering onShow. The diagram below:

Return the lifecycle order from PageB to PageA

Go to background

When you switch to the background, the applets and pages are not unloaded, only hidden. The onHide of the page is triggered first, followed by the onHide of the App. The diagram below:

Life cycle order when switching to background

Switch to foreground

When switching to the background, the applet triggers onShow first, followed by the page’s onShow. The diagram below:

Life cycle order when switching to foreground

Key Performance Indicators

After understanding the life cycle of each stage of the small program, we can work out the performance indicators of key nodes, as shown in the following table:

Record the data

If we record the interactive time data before and after the optimization of each page and compare them, we can better analyze the performance improvement of each page, so as to determine whether we are doing useless work.

From the above key performance indicators, the interactive time is extracted as one of the important evaluation indicators of this time, that is, the time taken by the whole process above after the page initiates an asynchronous request counting the onload event of the small program page, and the data is rendered to the page through setData after the request is returned.

However, a small program project often has many pages, and it is cumbersome to manually record the first screen time of each small program.

Therefore, we can rewrite the this.setData method to include point-in-time reporting logic.

this._startTime = new Date().getTime();
let fn = this.setData;
this.setData = (obj = {}, handle = ' ') = > {
	let now = new Date().getTime();
    // Report the time required for rendering
	log(now - this._startTime)
	fn.apply(this, [obj, handle]);
};
Copy the code

In addition, there are some performance indicators to be recorded. In the bus route display page, the loading time of pages when users pull down to the bottom and the time from clicking the search box to loading the search page are also important performance evaluation criteria. For this custom scenario, we can use the pair of functions console.time() and console.timeend () to record.

Index test

Test platform: Mi 8 SE, small program development tool

Test process: home -> Line -> drop down bottom -> click the search box

Test metrics: interactive time, page load time, page jump time

Optimized indexes:

platform Interactive time (MS) Page load time (ms) Page Jump time (MS)
Small program development tools 400 130 180
Mi 8 SE(Scan QR code for real machine debugging mode) 3000 110 1000

Among them, scanning two-dimensional code real machine debugging mode due to its own problems, the time becomes a normal phenomenon, in the automatic real machine debugging mode, the indicators return to normal, but because there is no exact data, so it is not included in the table.

Optimization result

Since then, the performance optimization of the line display page has been completed. In the actual optimization process, the following problems are found to have the greatest impact on performance

  • More pull-down loads, especially for special cards, were initially thought to be due to the fact that as more data was maintained in the pull-down page, after reducing the data maintained in the page instance, the performance improvement was not significant. Later, it was found that the scroll event was frequently triggered due to the need to monitor the Scroll event, and there were time-consuming operations in the callback function, resulting inonreachBottomThe event is blocked, meaning it takes about 1 to 2 seconds to initiate a request for the next page. cancelscrollEvent monitoring, performance is greatly improved. It comes down to small programsapiNo, frequent listening for scrollbar heightscrollEvents can be described as putting the cart before the horse.