Canvas 最常见的用途是渲染动画,目前,所有的主流浏览器都支持。
渲染动画的基本原理,无非是反复地擦除和重绘。为了动画的流畅,60Hz 刷新率设备的帧预算为 16.67ms,在这个时间之内,计算每个对象的位置、状态,还需要把它们都画出来。高刷设备的帧预算更低。所以需要时刻关注性能,防止计算和绘制耗时过长造成卡顿。
基础优化
1. 在离屏canvas上预渲染相似的图形或重复的对象
对于在每个动画帧上的重复相同的绘制操作,请考虑将其分流到屏幕外的画布上。然后根据需要频繁地将屏幕外图像渲染到主画布上,而不必首先重复生成该图像的步骤。
myEntity.offscreenCanvas = document.createElement("canvas");
myEntity.offscreenCanvas.width = myEntity.width;
myEntity.offscreenCanvas.height = myEntity.height;
myEntity.offscreenContext = myEntity.offscreenCanvas.getContext("2d");
myEntity.render(myEntity.offscreenContext);
2. 避免浮点数的坐标点,用整数取而代之
当画一个没有整数坐标点的对象时会发生子像素渲染。 浏览器为了达到抗锯齿的效果会做额外的运算。为了避免这种情况,请保证调用drawImage()
函数时,用 Math.floor()
函数对所有的坐标点取整。
ctx.drawImage(myImage, Math.floor(2.05), Math.floor(4.01));
3. 不要在用drawImage时缩放图像
在离屏canvas中缓存图片的不同尺寸,而不要用 drawImage()
去缩放它们。
4. 使用多层画布去画一个复杂的场景
若某些对象需要经常移动或更改,而其他对象则保持相对静态,可能的优化是使用多个<canvas>
元素对您的项目进行分层。
<div id="stage">
<!-- UI 层将仅在用户输入时发生变化 -->
<canvas id="ui-layer" width="480" height="320"></canvas>
<!-- 游戏层随每个新框架发生变化 -->
<canvas id="game-layer" width="480" height="320"></canvas>
<!-- 背景层通常保持不变 -->
<canvas id="background-layer" width="480" height="320"></canvas>
</div>
<style>
#stage {
width: 480px;
height: 320px;
position: relative;
border: 2px solid black
}
canvas { position: absolute; }
#ui-layer { z-index: 3 }
#game-layer { z-index: 2 }
#background-layer { z-index: 1 }
</style>
5. 大背景图用CSS设置
为了避免在每一帧在画布上绘制大图,可以用一个静态的<div>
元素,结合background
特性,以及将它置于画布元素之后。
6. 用CSS transforms特性缩放画布
CSS transforms 使用GPU,因此速度更快。最好的情况是不直接缩放画布,或者具有较小的画布并按比例放大,而不是较大的画布并按比例缩小:
const scaleX = window.innerWidth / canvas.width;
const scaleY = window.innerHeight / canvas.height;
const scaleToFit = Math.min(scaleX, scaleY);
csont scaleToCover = Math.max(scaleX, scaleY);
stage.style.transformOrigin = '0 0'; // scale from top left
stage.style.transform = 'scale(' + scaleToFit + ')';
7. 关闭透明度
若使用画布而且不需要透明,当使用 HTMLCanvasElement.getContext()
创建一个绘图上下文时把 alpha
选项设置为 false
。这个选项可以帮助浏览器进行内部优化:
const ctx = canvas.getContext('2d', { alpha: false });
画布渲染优化
1. 局部渲染缓存
进行局部渲染时,判断需要局部渲染的图形是否在视窗内可以极大加速渲染速度,而裁剪可以进一步分为分组的裁剪和图形的裁剪,而且需要缓存一些计算中间结果:
- 从画布->分组...-> 分组 到当前图形总的矩阵
- 图形在画布上的包围盒
- 图形当前是否在视窗内
- 单个图形发生过局部渲染,可以记录下受影响的图形,下次重新发生局部渲染可以直接使用缓存。本质上是缓存同单个图形相交的图形(成本太大,可以做渐进式缓存)
- 缓存一个时间段(16ms)内需要局部渲染的图形,进行一次统一的局部渲染
增加缓存可以有效提升渲染速度,但是当图形属性、父元素属性发生变化时清理缓存,这会增加额外的成本。
2. 是否放入局部刷新的队列
如果图形发生改变,首先检查其父元素是否发生改变,如果父元素发生改变,则不放入刷新队列中。因为父元素的改变在前时,已经加入刷新队列,或者其父元素的父元素已经在队列中。
如果图形变化前和变化后都不在视窗内,则也不应该放入刷新队列。
3. 减少包围盒的比对
如果元素未发生变化,则检测缓存的包围盒是否相交,如果不相交则停止检测。如果相交则递归检测其子元素是否同刷新的视窗相交。
4. 减少 Group 渲染
从循环遍历图形一轮,改成遍历两轮,第一轮先给每个 group,每个图形打标 refresh,如果有 refresh 标记第二次循环是绘制,否则跳过。
5. 控制渲染的频率
常用的技术主要有:debounce 和 throttle,保证 16ms 内必须要渲染一次。即在一段时间 16ms 内调用 n 次渲染,仅渲染最后那一次(方案1)或者渲染第一次和最后一次(方案2)。
优缺点:
- 方案1 相同时间内绘制的帧数低,如果在数据量非常大的情况下拾取时间 + 16ms 的延迟,能够出现明显的卡顿效果,两次渲染中间给交互留出了响应时间。
- 方案2 保证马上绘制:在测试时方便,不需要进行延迟测试;动画的执行也比较准确,绘制帧率高,相同时间内能够渲染的帧数要比第二种方案高