小程序Canvas性能優化實戰


以下內容轉載自totoro的文章《小程序Canvas性能優化實戰!》

作者:totoro

鏈接:https://blog.totoroxiao.com/canvas-perf-mini/

來源:https://blog.totoroxiao.com/

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

騰訊位置服務基於微信提供的小程序插件能力,專注於(圍繞)地圖功能,打造一系列小程序插件,可以幫助開發者簡單、快速的構建小程序,是您實現地圖功能的最佳伙伴。目前微信小程序插件提供路線規划、地鐵圖、地圖選點等服務!

案例背景

需求:

在小程序中使用canvas組件繪制地鐵圖,地鐵圖包括地鐵線路、站點圖標、線及站點名稱文字,繪制元素為線、圓、圖片、文字。
支持拖動平移和雙指縮放。

問題:

小程序中的canvas性能有限,特別在交互的過程中不斷觸發重繪會引發嚴重卡頓。

基本實現

在不考慮優化的情況下,先說說如何實現繪制和交互。

數據格式

首先看看數據,服務返回的數據中每個元素都是獨立的,包括該元素的樣式及坐標

// 線路數據
lineData = { path: [x0, y0, x1, y1, ...], strokeColor, strokeWidth }

// 站點數據:分為普通站點和換乘站點
// 普通站點繪制簡單圓形
stationData = { x, y, r, fillColor, strokeColor, strokeWidth }
// 換乘站點繪制換乘圖標(png圖片)
stationData_transfer = { x, y, width, height }

// 線路名稱
lineNameData = { text, x, y, fillColor }

// 站點名稱
stationNameData = { text, x, y }

繪圖API

繪制的時候遍歷繪制元素數組,根據元素類型設置上下文樣式,繪制及填充。接口參考:https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html

• 設置樣式:setStrokeStyle, setFillStyle, setLineWidth, setFontSize

• 繪制路線:moveTo, lineTo, stroke

• 繪制站點:moveTo, arc, stroke, fill

• 繪制圖片:drawImage

• 繪制文字:fillText

交互實現

實現交互主要步驟如下:

• 通過bindtouchstart、bindtouchmove、bindtouchend實現對用戶拖動和雙指縮放的監聽,得到拖動位移向量、縮放比例,觸發重繪。

• 繪制時通過scale和translate在不用對數據坐標進行處理的情況下實現縮放和平移

最終得到的結果如下,平均渲染時長為42.82ms,真機(ios)驗證:龜速移動,畫面延遲非常大。

優化方法

完全不了解canvas優化方案的同學可以先看看: canvas的優化

避免不必要的畫布狀態改變
參考Canvas 最佳實踐(性能篇) ,繪圖上下文是一個狀態機,狀態的改變是有一定開銷的。畫布狀態改變這里主要指strokeStyle、fillStyle等樣式的改變。

如何減少這部分的開銷呢?我們可以盡量讓樣式相同的元素放在一起進行一次性的繪制。觀察一下數據可以發現,很多站點元素樣式都是相同的,那么在繪制之前可以先做一次數據的聚合,將樣式相同的數據組合成一條數據:

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
}

聚合后,329條站點數據合並為24條,有效的減少了90%的冗余狀態改變開銷。修改之后測試一下,平均渲染時長降到了20.48ms,真機驗證:移動稍快了一些,但畫面仍有較高延遲。

合並數據的時候需要注意,此應用場景下各站點是沒有互相壓蓋的,而如果有壓蓋順序的話,在合並時只能合並相鄰且樣式相同的數據。

減少繪制物

• 篩除視野外的繪制物: 當用戶在放大圖像時,其實大部分繪制物都消失在了視野范圍之外,避免繪制視野外的元素可以節省不必要的開銷。點元素是比較容易判斷是否在視野范圍之外的,而站點、站點名、線路名都可以作為點元素處理;線路也可以計算出在視野范圍內的部分線段,較為復雜,這里先不做處理。篩除掉視野外的繪制物之后測試一下,平均渲染時長17.02ms,真機驗證:同上,沒有太多變化。

• 篩除過小的繪制物: 當用戶在縮小圖像時,文字和站點會由於尺寸太小而看不大清,在不影響用戶體驗的前提下可以考慮直接去掉。根據測試,最終決定在顯示比例小於30%時去除文字和站點,這個級別下的渲染時長從22.12ms,減少到了9.68ms。

降低重繪頻率

雖然平均渲染時長已經低了很多,但是在交互時卻仍有較高的延遲,這是因為每次ontouchmove都會將渲染任務加入到異步隊列中,事件觸發頻率遠高於每秒能夠執行的渲染次數,導致渲染任務嚴重積壓,不斷滯后。在PC端一般使用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);
};

PC端我們一般將渲染間隔控制在16ms左右,但是在小程序中考慮到性能限制,且移動端各機型性能不一,所以這里留了一些空間,控制在30ms,對應到30FPS左右。

但如果一直循環調用也會造成靜止狀態下不必要的開銷,所以可以在交互開始ontouchstart和結束ontouchend時分別開啟、停止動畫:

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

stop() {
  cancelAnimationFrame(this.animateId)
}

修改之后真機驗證一下:畫面比較流程,有輕微卡頓,但不會延遲。

其他注意

由於本例中縮放和平移狀態是以絕對狀態保存的,所以scale和translate要搭配save和restore一起使用;但也可以使用setTransform直接重置矩陣。從理論上看這樣應該能節省開銷,但實際測試並沒什么效果,平均渲染時長在18.12ms。這個問題有待研究。
小程序中避免使用setData保存與界面渲染無關的數據,以避免引起頁面重繪。

優化結果

經過以上優化,渲染時長從42降到了17ms左右,真機驗證下安卓機型普遍非常流暢,體驗很好;ios機型有輕微卡頓,且隨着使用時長卡頓逐漸明顯,后期可以深入研究下是否有內存管理的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM