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

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

相关推荐
zhanghaisong_20152 分钟前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉5 分钟前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七35 分钟前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php
老赵的博客1 小时前
QSS 设置bug
前端·bug·音视频
Chikaoya1 小时前
项目中用户数据获取遇到bug
前端·typescript·vue·bug
南城夏季1 小时前
蓝领招聘二期笔记
前端·javascript·笔记
Huazie1 小时前
来花个几分钟,轻松掌握 Hexo Diversity 主题配置内容
前端·javascript·hexo
NoloveisGod1 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing1 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
海上彼尚2 小时前
实现3D热力图
前端·javascript·3d