【性能优化】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的环境下,使用时需要注意兼容性的问题。这个功能的最大优势在于,在渲染的过程中并不会阻塞主线程,可以通过 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图展示:

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

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

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

总结:

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

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

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

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

相关推荐
卸任4 小时前
Vibe coding工具使用心得
前端·ai编程
brien94 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance5 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M5 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写5 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李5 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易5 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马5 小时前
前沿视界
前端
kyriewen5 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易5 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员