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

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

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95272 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大2 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师2 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学2 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端