小程序Canvas性能優化實戰

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

做者:totorohtml

連接:https://blog.totoroxiao.com/c...ios

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

著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。canvas

騰訊位置服務基於微信提供的小程序插件能力,專一於(圍繞)地圖功能,打造一系列小程序插件,能夠幫助開發者簡單、快速的構建小程序,是您實現地圖功能的最佳夥伴。目前微信小程序插件提供路線規劃、地鐵圖、地圖選點等服務,歡迎你們體驗!
咱們將陸續推出更多功能的插件,敬請期待!小程序

案例背景

需求:

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

問題:

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

基本實現

在不考慮優化的狀況下,先說說如何實現繪製和交互。數組

數據格式

首先看看數據,服務返回的數據中每一個元素都是獨立的,包括該元素的樣式及座標性能優化

// 線路數據
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機型有輕微卡頓,且隨着使用時長卡頓逐漸明顯,後期能夠深刻研究下是否有內存管理的問題。
after.gif

相關文章
相關標籤/搜索