【性能优化】Offscreencanvas/离屏canvas渲染介绍

Canvas 元素是在 web 开发中通常用来绘制图形的元素,他可以帮我们实现各式各样的图像并可以在前端实现 3d图像展示,图片裁剪,动画效果。但是 canvas 的逻辑与渲染发生在主线程中,所以在用重用户交互的场景下, canvas 渲染逻辑过于沉重会在应用实时性与性能上有着负面影响。在目前也有这不同的性能优化手段:

  • 离屏渲染:使用一个不可见的 canvas 对即将渲染的内容进行绘制然后同步渲染到主画布上

在开发中,我们可以通过 document.createElement('canvas') 创建一个 canvas 元素来作为备用渲染元素并提前绘制好某个图像,在画布更新的时候直接使用已经创建的 canvas 元素来更新主 canvas 上绘制的内容。一些应用的场景:

  1. 提前绘制特定内容:避免重复生成的开销
  2. 使用双 canvas 交替绘制,在分页绘制的场景下,离屏绘制下一页/下一屏的内容,切换的时候直接使用绘制好的内容。

上面两种情况都可以做到预渲染的效果,但 canvas 元素还都是在 DOM的结构下,并且在一定程度上会阻塞主线程。

而今天介绍的 OffscreenCanvas 则可以做到真正的离屏渲染,可以将 canvas 在浏览器 worker 线程中进行处理,减少主线程的性能开销。

Offscreen Canvas 简介与使用:

  • OffscreenCanvas 提供了一个可以脱离屏幕渲染的 Canvas 对象,可以运行在DOM或 web worker的环境下,使用时需要注意兼容性的问题。这个功能的最大优势在于,在渲染的过程中并不会阻塞主线程,可以通过 OffscreenCanvasworker 更新画布内容,避免造成卡顿的体验。
    • 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图展示:

上述代码一共做了这件几件事:

  1. 定义两个canvas,通过 arcrequestAnimationFrame 制作圆形动画
  2. 定义递归版本斐波那契数列函数用来模拟耗时操作
  3. canvasTwo 通过 offscreenCanvas 来绘制

通过点击 求解斐波那契数列 按钮我们可以发现,离屏渲染的canvas还是在渲染中,而正常DOM下的canvas由于主 线程的占用而停止了渲染。

总结:

在不同的业务中,我们可以通过分析业务场景来使用 offscreenCanvasweb worker 来优化渲染效果,但需要注意,离屏canvas的兼容性。

附上Canvas 其他性能优化方案:

  • 避免全量渲染,尽量增量渲染
  • 使用 CSS transform 代替 Canvas 计算缩放
  • 将复杂的任务进行拆分,避免长时间计算造成页面卡顿

希望这篇文章对你有帮助~ 有任何问题可以在下方留言或私信,欢迎沟通与交流。

相关推荐
小满zs35 分钟前
React-router v7 第一章(安装)
前端·react.js
程序员小续41 分钟前
前端低代码架构解析:拖拽 UI + 代码扩展是怎么实现的?
前端·javascript·面试
wangpq1 小时前
微信小程序地图callout气泡图标在ios显示,在安卓机不显示
前端·vue.js
curdcv_po1 小时前
Vue3 组件通信方式全解析
前端
Auroral1561 小时前
基于RabbitMQ的异步通知系统设计与实现
前端·后端
栗筝i1 小时前
Spring 核心技术解析【纯干货版】- XV:Spring 网络模块 Spring-Web 模块精讲
前端·网络·spring
打野赵怀真1 小时前
H5如何禁止动画闪屏?
前端·javascript
zhangxingchao1 小时前
关于浮点数的思考
前端
Riesenzahn1 小时前
你喜欢Sass还是Less?为什么?
前端·javascript