The following content is reprinted from totoro’s article “Small program Canvas Performance Optimization!”

Author: totoro

Link: blog.totoroxiao.com/canvas-perf…

Source: blog.totoroxiao.com/

Copyright belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.

Tencent location services based on the small program plug-in capabilities provided by wechat, focusing on (around) map function, to create a series of small program plug-ins, can help developers to build simple and fast small program, is your best partner to achieve map function. Currently wechat small program plug-in providedRoute planning, subway map, map selectionAnd other services, you are welcome to experience! We will introduce more functions of the plug-in, please look forward to!

Case background

Requirements:

In the small program, canvas component is used to draw subway map. Subway map includes subway line, station icon, line and station name text, and drawing elements are line, circle, picture and text. Support drag pan and double finger zoom.

Question:

Canvas performance in the small program is limited, especially in the process of interaction constantly triggered redraw will cause serious lag.

Basic implementation

Without thinking about optimization, let’s talk about how to implement drawing and interaction.

The data format

Looking at the data first, each element in the data returned by the service is independent, including the style and coordinates of that element

// lineData = {path: [x0, y0, x1, y1... , strokeColor, strokeWidth} StationData = {x, y, r, fillColor, strokeColor, StationData_transfer = {x, y, width, height} lineNameData = {text, x, y, FillColor} // stationNameData = {text, x, y}Copy the code

Drawing API

Draw while traversing the draw element array, setting the context style according to the element type, draw and fill. The interface reference: developers.weixin.qq.com/miniprogram…

• Set styles: setStrokeStyle, setFillStyle, setLineWidth, setFontSize

• Draw routes: moveTo, lineTo, stroke

• Draw sites: moveTo, Arc, Stroke, Fill

• Draw an image: drawImage

• Draw text: fillText

interactions

The main steps of interaction are as follows:

• Through bindTouchStart, BindTouchMove, bindTouchEnd to realize the user drag and double finger zoom monitoring, get drag displacement vector, zoom ratio, trigger redraw.

• Scale and translate are used when drawing to scale and translate without processing data coordinates

The final result is as follows, with an average render time of 42.82ms, real (ios) verification: slow motion, very large lag.

An optimization method

For those of you who are completely unfamiliar with canvas optimization, you can take a look at: Canvas optimization.

Avoid unnecessary Canvas state changes Refer to Canvas Best Practices (Performance). The drawing context is a state machine, and state changes have some overhead. The change of canvas state mainly refers to the change of strokeStyle, fillStyle and other styles.

How do you reduce the cost of this? We can try to put elements of the same style together for a one-time drawing. Look at the data can be found, many site elements are the same style, so before drawing can do a data aggregation, will be the same style of data into a data:

function mergeStationData(mapStation) {
  let mergedData = {}

  mapStation.forEach(station => {
    let coord = `${station.x}.${station.y}.${station.r}`
    let stationStyle = `${station.fillColor}|${station.strokeColor}|${station.strokeWidth}`

    if (mergedData[stationStyle]) {
      mergedData[stationStyle].push(coord)
    } else {
      mergedData[stationStyle] = [coord]
    }
  })

  return mergedData
}
Copy the code

After aggregation, 329 site data were merged into 24, effectively reducing the cost of redundant state change by 90%. After modification, the average rendering time is reduced to 20.48ms. Real machine verification: moving a little faster, but the picture still has a high latency.

When merging data, note that in this scenario, sites are not overlapped with each other, and if there is a overlapped sequence, only adjacent data with the same style can be merged.

Reduce drawing

• Screen out out-of-view artwork: When the user enlarges an image, most of the artwork disappears out of view. Avoiding out-of-view artwork can save unnecessary overhead. The point element is relatively easy to judge whether it is out of the field of view, and the site, site name, line name can be treated as the point element; The line can also calculate part of the line segment in the field of vision, which is more complicated and will not be dealt with here. After screening out the out-of-field objects, the average rendering time was 17.02ms. Real machine verification: same as above, not much changed.

• Screen out drawings that are too small: When the user is zooming in on the image, the text and site will be too small to see clearly, so it can be directly removed without affecting the user experience. Based on testing, it was decided to remove text and sites at a display ratio of less than 30%, and rendering times at this level were reduced from 22.12ms to 9.68ms.

Reduce the redraw frequency

Although the average rendering time is much lower, there is still a high latency in the interaction, because each onTouchMove will add the rendering task to the asynchronous queue, and the event trigger frequency is much higher than the number of renders that can be performed per second, resulting in a serious backlog of rendering tasks and continuous lag. RequestAnimationFrame = requestAnimationFrame = requestAnimationFrame = requestAnimationFrame

const requestAnimationFrame = function (callback, lastTime) {
  var lastTime;
  if (typeof lastTime === 'undefined') {
    lastTime = 0
  }
  var currTime = new Date().getTime();
  var timeToCall = Math.max(0, 30 - (currTime - lastTime));
  lastTime = currTime + timeToCall;
  var id = setTimeout(function () {
    callback(lastTime);
  }, timeToCall);
  return id;
};

const cancelAnimationFrame = function (id) {
  clearTimeout(id);
};
Copy the code

We generally control the render interval at about 16ms on PC, but considering the performance limitation in the small program, and the performance of different models on mobile terminal is different, so we leave some space here, control at 30ms, corresponding to about 30FPS.

However, looping through the call can also cause unnecessary overhead in the static state, so you can start and stop the animation at the beginning of onTouchStart and the end of onTouchEnd:

animate(lastTime) {
  this.animateId = requestAnimationFrame((t) => {
    this.render()
    this.animate(t)
  }, lastTime)
},

stop() {
  cancelAnimationFrame(this.animateId)
}
Copy the code

After modifying the real machine to verify: screen process, there is a slight delay, but there is no delay.

Other note

Since the scale and translation states are saved in absolute terms in this example, scale and translate are used together with save and restore; But you can also reset the matrix directly using setTransform. In theory this should save money, but the actual testing didn’t work, with an average render time of 18.12ms. The question remains to be studied. Avoid using setData in small programs to save data unrelated to interface rendering to avoid page redrawing.

The optimization results

After the above optimization, the rendering time is reduced from 42 to about 17ms. Android models are generally very smooth and have a good experience when verified by the real machine. Ios models have slight lag, and the lag is gradually obvious with the use of time. Later, we can further study whether there is a memory management problem.