简介
H5中引入了对canvas的支持,使得网页的表达能力更加丰富了。程序员可以通过canvas来绘制复杂的图形,甚至是游戏。因为工作中的需求,需要使用canvas在网页上绘制家谱。具体的算法可以看之前的一篇总结:树的可视化以及家谱绘制的算法
。当家谱中的数据量变大之后,整个绘制过程会变的很卡(800左右人物的家谱1000次绘制居然需要25s左右)。但是在做完优化后,1000次绘制只需要0.15s左右。
目前的家谱绘制共有两部分。一个是家谱的主体,用户可以通过鼠标、滚轮去任意的浏览;一个是左上角的小地图功能,帮助用户了解到当前浏览的位置,小地图中的有色方块就是用户整个屏幕显示的内容。
优化措施一:缓存
缓存是在绝大多数系统中经常用到的提升性能的方法,典型的以空间换时间。在canvas中当然也可以使用缓存。在家谱的每一次绘制中,都要遍历所有的人物,然后计算他们的位置大小,然后绘制到界面上,然后还要遍历所有的路径再次进行绘制。而使用缓存的目的就是避免这一部分的计算。因此,在任何复杂计算后的绘制,都能使用缓存来提升性能。
在canvas中有这样一个方法,CanvasRenderingContext2D.drawImage(image, dx, dy)
,image
参数代表要绘制的图片源,不仅仅可以是一个image对象,还可以是一个canvas对象。所以如果我们将一个复杂的图形作为canvas对象缓存起来,然后直接使用drawImage
方法去绘制到画面上,就能减少很多的重复计算,来提升性能。
例如以下的伪代码
function paintPerson() {
// paint
}
function paintFamilyTree() {
for(var i = 0; i < 10000; i++) {
paintPerson()
}
}
// 用户移动鼠标就需要重绘族谱
dom.addEventListener("mousemove", function() {
paintFamilyTree()
}
用户每一次移动鼠标,都需要10000次的循环去画。所以我们使用下面的方法,使得只需要计算一次
function paintPerson() {
// paint
}
function paintFamilyTree() {
for(var i = 0; i < 10000; i++) {
paintPerson()
}
}
var canvasObj = cache(paintFamilyTree) // 缓存这次绘制的结果。
dom.addEventListener("mousemove", function() {
ctx.drawImage(canvasObj,0,0)
}
那么如何去缓存这个 canvas 对象呢?可以使用document.createElement('canvas')
来创建一个不在页面上显示的 canvas 对象,一般也称它为离屏画布
,这里用变量 offScreenCanvas 来称呼。然后将绘制的图形画在这个 canvas 上,之后调用 ctx.drawImage(offScreenCanvas,0,0)
即可。
在创建这个离屏画布的时候,我们也要设置合适的宽高,这样也有助于提升性能。
可以参考这个例子来感受一下性能差距:
离屏画布缓存来提升页面性能
例子代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>离屏canvas实例</title>
<style>
.main-wrapper {
width: 800px;
margin: 10px auto;
}
</style>
</head>
<body onload="init()">
<div class="main-wrapper">
<canvas width="800" height="600" style="border: 1px solid #ccc" id="canvas">
你的浏览器不支持canvas
</canvas>
<br><br><br><br><br><br><br><br><br>
<div id="result"></div>
<br>
渲染次数:<input type="number" id="times" value="1000">,共耗时(ms)<span id="time_used">0</span><br>
<button onclick="doTest(false,false)">不使用离屏canvas</button>
<button onclick="doTest(true,false)">使用离屏canvas</button>
<button onclick="doTest(true,true)">使用离屏canvas并设置正确的高度</button>
</div>
<script>
/**
* 离屏缓存,num为缓存canvas的数量
*/
var OffScreenCache = function (num) {
this.canvases = [];
for (i = 0; i < num; i++) {
this.canvases.push(document.createElement("canvas"));
}
}
OffScreenCache.prototype = {
pop: function() {
return this.canvases.pop();
},
push: function(canvas) {
this.canvases.push(canvas);
},
destroy: function() {
this.canvases = null;
}
}
var Ball = function (color) {
this.radius = 50;
this.lineWidth = 4;
this.cache = null;
this.color = color;
}
Ball.prototype = {
paint: function(ctx, x, y) {
ctx.save();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.color;
for (i = 1; i < this.radius; i+= this.lineWidth) {
ctx.beginPath();
ctx.arc(x, y, i, 0, Math.PI*2,true); // 绘制
ctx.stroke();
}
ctx.restore();
},
useCache: function (cacheCanvas, autoSet) {
if (autoSet) {
cacheCanvas.width = this.radius * 2;
cacheCanvas.height = this.radius * 2;
}
cacheCtx = cacheCanvas.getContext('2d')
this.paint(cacheCtx, cacheCanvas.width / 2, cacheCanvas.height / 2)
this.cache = cacheCanvas
}
}
/******************** 下面是执行代码 **************************/
var g_canvas, g_ctx;
var g_width = 800;
var g_height = 600;
function init() {
g_canvas = document.getElementById("canvas");
g_ctx = g_canvas.getContext('2d');
}
function getRandomPos() {
var x = Math.random() * g_width
var y = Math.random() * g_height
return {x:x, y:y}
}
function showResult(ballCount) {
var dom = document.getElementById("result");
dom.innerHTML = "";
var str = "";
for (var key of Object.keys(ballCount)) {
str += "<span>" + key + "ball:" + ballCount[key] + "</span> "
}
dom.innerHTML = str;
}
function doTest(useCache, autoSet) {
g_ctx.clearRect(0, 0, g_width, g_height)
var startTime = Date.now();
var times = document.getElementById("times").value
var colorArray = ['red', 'blue', 'green', 'black', 'pink'] // 共绘制5种颜色
var ballCount = {};
colorArray.forEach(function(color) {
ballCount[color] = 0;
})
if (useCache) {
var colorBall = [];
var cacheCanvases = new OffScreenCache(colorArray.length);
for (var i = 0; i < colorArray.length; i++) {
var ball = new Ball(colorArray[i]);
var cacheCanvas = cacheCanvases.pop();
ball.useCache(cacheCanvas, autoSet)
colorBall.push(ball)
}
for (var i = 0; i < times; i++) {
ball = colorBall[i % 5]
ballCount[ball.color]++;
var pos = getRandomPos()
g_ctx.drawImage(ball.cache, pos.x, pos.y) // 开始画
}
} else {
for (var i = 0; i < times; i++) {
var color = colorArray[i % 5];
var ball = new Ball(color)
var pos = getRandomPos()
ball.paint(g_ctx, pos.x, pos.y) // 开始画
ballCount[color]++;
}
}
var endTime = Date.now();
document.getElementById("time_used").innerText = endTime - startTime;
showResult(ballCount)
}
</script>
</body>
</html>
优化措施二:分层
在上述的族谱图片中,左上角的小地图其实是有两个canvas重叠而成。底层的canvas绘制族谱的缩略图,上层的canvas绘制小方框。这样在小方框移动时,只需清空上层canvas的局部画布重绘小方框即可,不需要重绘底层的缩略图。