Canvas
元素是在 web 开发中通常用来绘制图形的元素,他可以帮我们实现各式各样的图像并可以在前端实现 3d图像展示,图片裁剪,动画效果。但是 canvas
的逻辑与渲染发生在主线程中,所以在用重用户交互的场景下, canvas
渲染逻辑过于沉重会在应用实时性与性能上有着负面影响。在目前也有这不同的性能优化手段:
- 离屏渲染:使用一个不可见的
canvas
对即将渲染的内容进行绘制然后同步渲染到主画布上
在开发中,我们可以通过 document.createElement('canvas')
创建一个 canvas 元素来作为备用渲染元素并提前绘制好某个图像,在画布更新的时候直接使用已经创建的 canvas
元素来更新主 canvas
上绘制的内容。一些应用的场景:
- 提前绘制特定内容:避免重复生成的开销
- 使用双
canvas
交替绘制,在分页绘制的场景下,离屏绘制下一页/下一屏的内容,切换的时候直接使用绘制好的内容。
上面两种情况都可以做到预渲染的效果,但 canvas
元素还都是在 DOM的结构下,并且在一定程度上会阻塞主线程。
而今天介绍的 OffscreenCanvas
则可以做到真正的离屏渲染,可以将 canvas
在浏览器 worker
线程中进行处理,减少主线程的性能开销。
Offscreen Canvas 简介与使用:
OffscreenCanvas
提供了一个可以脱离屏幕渲染的Canvas
对象,可以运行在DOM或 web worker的环境下,使用时需要注意兼容性的问题。这个功能的最大优势在于,在渲染的过程中并不会阻塞主线程,可以通过OffscreenCanvas
和worker
更新画布内容,避免造成卡顿的体验。transferControlToOffscreen
: 将控制转移到一个在主线程或者 web worker 的OffscreenCanvas
对象上。
直接上代码: JSX:
jsx
import React, { useEffect, useRef } from "react";
import './index.scss';
const fib = (n) => {
if (n <= 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
// 定义 worker thread来渲染 canvasTwo (语法为webpack本地的写法)
const canvasWorker = new Worker(new URL("./worker.js", import.meta.url));
export const AnimationCanvas = () => {
const canvasOneRef = useRef(null);
const canvasTwoRef = useRef(null);
useEffect(() => {
const canvas = canvasOneRef.current;
const ctx = canvas.getContext('2d');
let frameCount = 0;
let animationFrameId;
const render = () => {
frameCount++;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = '#000000';
ctx.beginPath();
ctx.arc(150, 150, 20 * Math.sin(frameCount * 0.05) ** 2, 0, 2 * Math.PI);
ctx.fill();
animationFrameId = window.requestAnimationFrame(render);
}
render();
const transfercanvasWorker = canvasTwoRef.current.transferControlToOffscreen();
canvasWorker.postMessage({ canvas: transfercanvasWorker }, [transfercanvasWorker]);
return () => {
window.cancelAnimationFrame(animationFrameId);
}
}, [])
const alertFib = () => {
alert(fib(40));
}
return (
<div className="container">
<div>
<canvas ref={canvasOneRef} width={300} height={300} />
<span>正常渲染Canvas</span>
</div>
<div>
<canvas ref={canvasTwoRef} width={300} height={300} />
<span>离屏渲染Canvas</span>
</div>
<button onClick={alertFib}>求解斐波那契数列</button>
</div>
)
}
Worker文件:
javascript
let canvasB = null;
let ctxWorker = null;
let frameId = null;
self.onmessage = (e) => {
canvasB = e.data.canvas;
ctxWorker = canvasB.getContext("2d");
drawCanvas();
}
let frameCount = 0;
function drawCanvas() {
frameCount++;
ctxWorker.clearRect(0, 0, ctxWorker.canvas.width, ctxWorker.canvas.height);
ctxWorker.fillStyle = '#000000';
ctxWorker.beginPath();
ctxWorker.arc(150, 150, 20 * Math.sin(frameCount * 0.05) ** 2, 0, 2 * Math.PI);
ctxWorker.fill();
frameId = self.requestAnimationFrame(drawCanvas); /* 之后可以通过cancelAnimationFrame将动画取消 */
}
index.scss:
scss
.container {
width: 100%;
height: 100%;
background-color: azure;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
& > div {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 20px;
}
button {
margin-top: 20px;
padding: 10px 20px;
background-color: #f1f1f1;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background-color: #e1e1e1;
}
}
}
运行时GIF图展示:
上述代码一共做了这件几件事:
- 定义两个canvas,通过
arc
与requestAnimationFrame
制作圆形动画 - 定义递归版本斐波那契数列函数用来模拟耗时操作
- canvasTwo 通过
offscreenCanvas
来绘制
通过点击 求解斐波那契数列
按钮我们可以发现,离屏渲染的canvas还是在渲染中,而正常DOM下的canvas由于主 线程的占用而停止了渲染。
总结:
在不同的业务中,我们可以通过分析业务场景来使用 offscreenCanvas
与 web worker
来优化渲染效果,但需要注意,离屏canvas的兼容性。
附上Canvas 其他性能优化方案:
- 避免全量渲染,尽量增量渲染
- 使用
CSS transform
代替Canvas
计算缩放 - 将复杂的任务进行拆分,避免长时间计算造成页面卡顿
希望这篇文章对你有帮助~ 有任何问题可以在下方留言或私信,欢迎沟通与交流。