小程序中的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机型有轻微卡顿,且随着使用时长卡顿逐渐明显,后期能够深刻研究下是否有内存管理的问题。