【性能优化】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 计算缩放
  • 将复杂的任务进行拆分,避免长时间计算造成页面卡顿

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

相关推荐
arvin_xiaoting5 小时前
OpenClaw学习总结_I_核心架构_8:SessionPruning详解
前端·chrome·学习·系统架构·ai agent·openclaw·sessionpruning
工程师老罗6 小时前
Image(图像)的用法
java·前端·javascript
swipe7 小时前
把 JavaScript 原型讲透:从 `[[Prototype]]`、`prototype` 到 `constructor` 的完整心智模型
前端·javascript·面试
问道飞鱼7 小时前
【前端知识】React 组件生命周期:从底层原理到实践场景
前端·react.js·前端框架·生命周期
CHU7290357 小时前
定制专属美丽时刻:美容预约商城小程序的贴心设计
前端·小程序
浩~~8 小时前
反射型XSS注入
前端·xss
AwesomeDevin8 小时前
AI时代,我们的任务不应沉溺于与 AI 聊天,🤔 从“对话式编程”迈向“数字软件工厂”
前端·后端·架构
harrain8 小时前
antvG2折线图和区间range标记同时绘制
前端·javascript·vue.js·antv·g2
德育处主任Pro8 小时前
从重复搭建到高效生产,RollCode的H5开发新范式
前端
蜡台9 小时前
SPA(Single Page Application) Web 应用(即单页应用)架构模式 更新
前端·架构·vue·react·spa·spa更新