小程序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)验证:龟速移动,画面延迟非常大。


image

优化方法

完全不了解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机型有轻微卡顿,且随着使用时长卡顿逐渐明显,后期可以深入研究下是否有内存管理的问题。

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容