小程序中的canvas性能有限,特別在交互的過程當中不斷觸發重繪會引起嚴重卡頓。html
在不考慮優化的狀況下,先說說如何實現繪製和交互。ios
首先看看數據,服務返回的數據中每一個元素都是獨立的,包括該元素的樣式及座標web
// 線路數據
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 }
複製代碼
繪製的時候遍歷繪製元素數組,根據元素類型設置上下文樣式,繪製及填充。接口參考:developers.weixin.qq.com/miniprogram…canvas
實現交互主要步驟以下:小程序
bindtouchstart
、bindtouchmove
、bindtouchend
實現對用戶拖動和雙指縮放的監聽,獲得拖動位移向量、縮放比例,觸發重繪scale
和translate
在不用對數據座標進行處理的狀況下實現縮放和平移最終獲得的結果以下,平均渲染時長爲42.82
ms,真機(ios)驗證:龜速移動,畫面延遲很是大。微信小程序
徹底不瞭解canvas優化方案的同窗能夠先看看: canvas的優化。api
參考Canvas 最佳實踐(性能篇) ,繪圖上下文是一個狀態機,狀態的改變是有必定開銷的。畫布狀態改變這裏主要指strokeStyle
、fillStyle
等樣式的改變。數組
如何減小這部分的開銷呢?咱們能夠儘可能讓樣式相同的元素放在一塊兒進行一次性的繪製。觀察一下數據能夠發現,不少站點元素樣式都是相同的,那麼在繪製以前能夠先作一次數據的聚合,將樣式相同的數據組合成一條數據:bash
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.48
ms,真機驗證:移動稍快了一些,但畫面仍有較高延遲。微信
合併數據的時候須要注意,此應用場景下各站點是沒有互相壓蓋的,而若是有壓蓋順序的話,在合併時只能合併相鄰且樣式相同的數據。
篩除視野外的繪製物:
當用戶在放大圖像時,其實大部分繪製物都消失在了視野範圍以外,避免繪製視野外的元素能夠節省沒必要要的開銷。點元素是比較容易判斷是否在視野範圍以外的,而站點、站點名、線路名均可以做爲點元素處理;線路也能夠計算出在視野範圍內的部分線段,較爲複雜,這裏先不作處理。篩除掉視野外的繪製物以後測試一下,平均渲染時長17.02
ms,真機驗證:同上,沒有太多變化。
篩除太小的繪製物:
當用戶在縮小圖像時,文字和站點會因爲尺寸過小而看不大清,在不影響用戶體驗的前提下能夠考慮直接去掉。根據測試,最終決定在顯示比例小於30%時去除文字和站點,這個級別下的渲染時長從22.12
ms,減小到了9.68
ms。
雖然平均渲染時長已經低了不少,可是在交互時卻仍有較高的延遲,這是由於每次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.12
ms。這個問題有待研究。setData
保存與界面渲染無關的數據,以免引發頁面重繪。通過以上優化,渲染時長從42
降到了17
ms左右,真機驗證下安卓機型廣泛很是流暢,體驗很好;ios機型有輕微卡頓,且隨着使用時長卡頓逐漸明顯,後期能夠深刻研究下是否有內存管理的問題。