You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JS Canvas动画在Firefox仅1FPS性能极低,Chromium正常求优化

Firefox下Canvas贝塞尔曲线动画帧率优化方案

我用2D Canvas实现了带动态背景的3条贝塞尔曲线旋转动画,在Chromium内核浏览器运行正常,但Firefox里帧率仅1FPS,尝试多种优化后仅提升至1.2FPS,无法满足需求。以下是针对性优化方案:

核心优化点

  • 合并路径绘制,减少上下文切换
    原代码每条贝塞尔曲线单独调用beginPath和stroke,且重复设置strokeStyle,这会触发大量Canvas上下文状态更新。将同色路径合并为一个路径后一次性绘制,能大幅减少状态切换开销。

  • 缓存渐变对象,避免重复创建
    原background()方法每次渲染都重新创建线性渐变,Firefox对这类对象的创建开销敏感。缓存渐变实例,仅在颜色或画布尺寸变化时重新生成。

  • 提取固定计算,避免重复运算
    将循环内的固定值计算(如radius/3、旋转角度转弧度)移到循环外,减少每帧的重复计算量。

  • 优化数组操作
    原代码用shift()移除数组首元素,该操作会导致数组内所有元素前移,性能开销大。改用slice(1)替代,或使用环形数组指针进一步优化。

  • 调整渲染循环逻辑
    修正requestAnimationFrame的调用方式,避免递归调用可能导致的帧堆积;不可见时完全停止渲染流程,而非仅跳过绘制步骤。

修改后核心代码示例

class BG {
    animationContainer = null;
    options = {
        rotation: 45,
        waves: 3,
        width: 300,
        hue: [14, 50],
        amplitude: .5,
        background: 1,
        preload: 1,
        speed: [.001, .004]
    };
    waves = [];
    hue = 0;
    hueFw = true;
    canvas = null;
    ctx = null;
    observer = null;
    radius = 0;
    centerX = 0;
    centerY = 0;
    width = 0;
    height = 0;
    scale = 1;
    isVisible = false;
    gradient = null;
    color = "#000000";
    lastColor = "";
    lastHeight = 0;

    constructor(t, e) {
        this.animationContainer = t;
        this.options = {
            rotation: e.rotation || 45,
            waves: e.waves || 2,
            width: e.width || 300,
            hue: e.hue || [17, 50],
            amplitude: e.amplitude || .75,
            background: e.background || 1,
            preload: e.preload || 1,
            speed: e.speed || [.001, .004]
        };
        this.waves = [];
        this.hue = this.options.hue[0];
        this.hueFw = true;
        this.setup();
        this.render();
    }

    setup() {
        this.canvas = document.createElement("canvas");
        this.ctx = this.canvas.getContext("2d");
        this.animationContainer.appendChild(this.canvas);
        this.resize();
        window.addEventListener("resize", this.resize.bind(this));
        this.waves = [];
        for (let t = 0; t < this.options.waves; t++) this.waves.push(WV(this));
        this.options.preload && this.preload();
        this.addIntersection();
    }

    resize() {
        const { offsetWidth: t, offsetHeight: e } = this.animationContainer;
        this.scale = window.devicePixelRatio || 1;
        this.width = t * this.scale;
        this.height = e * this.scale;
        this.canvas.width = this.width;
        this.canvas.height = this.height;
        this.canvas.style.width = `${t}px`;
        this.canvas.style.height = `${e}px`;
        this.radius = Math.sqrt(Math.pow(this.width, 2) + Math.pow(this.height, 2)) / 2;
        this.centerX = this.width / 2;
        this.centerY = this.height / 2;
    }

    addIntersection() {
        this.observer = new IntersectionObserver(t => {
            for (let e of t) this.isVisible = e.intersectionRatio > 0;
        });
        this.observer.observe(this.animationContainer);
    }

    preload() {
        for (let t = 0; t < this.options.waves; t++) {
            this.updateColor();
            for (let e = 0; e < this.options.width; e++) this.waves[t].update();
        }
    }

    updateColor() {
        this.hue += this.hueFw ? .01 : -.01;
        if (this.hue > this.options.hue[1] && this.hueFw) this.hueFw = false;
        else if (this.hue < this.options.hue[0] && !this.hueFw) this.hueFw = true;
        
        const t = Math.floor(127 * Math.sin(.3 * this.hue) + 128);
        const e = Math.floor(127 * Math.sin(.3 * this.hue + 2) + 128);
        const i = Math.floor(127 * Math.sin(.3 * this.hue + 4) + 128);
        this.color = `rgba(${t},${e},${i},0.1)`;
    }

    render() {
        const renderFrame = () => {
            if (this.isVisible) this.draw();
            requestAnimationFrame(renderFrame);
        };
        requestAnimationFrame(renderFrame);
    }

    draw() {
        this.updateColor();
        this.clear();
        if (this.options.background) this.background();
        for (let t of this.waves) {
            t.update();
            t.draw();
        }
    }

    clear() {
        this.ctx.clearRect(0, 0, this.width, this.height);
    }

    background() {
        if (!this.gradient || this.lastColor !== this.color || this.lastHeight !== this.height) {
            this.gradient = this.ctx.createLinearGradient(0, 0, 0, this.height);
            this.gradient.addColorStop(1, "#0E1218");
            this.gradient.addColorStop(0, this.color);
            this.lastColor = this.color;
            this.lastHeight = this.height;
        }
        this.ctx.fillStyle = this.gradient;
        this.ctx.fillRect(0, 0, this.width, this.height);
    }
}

function LN(t, e) {
    t.angle[0] += t.speed[0];
    t.angle[1] += t.speed[1];
    t.angle[2] += t.speed[2];
    t.angle[3] += t.speed[3];
    return { 
        angle: [
            Math.sin(t.angle[0]), 
            Math.sin(t.angle[1]), 
            Math.sin(t.angle[2]), 
            Math.sin(t.angle[3])
        ], 
        wave: t, 
        color: e 
    };
}

function rng(t, e) {
    return e === undefined ? Math.random() * t : t + Math.random() * (e - t);
}

function dr() {
    return Math.random() > .5 ? 1 : -1;
}

function WV(t) {
    let e = [];
    const angles = [
        rng(2 * Math.PI),
        rng(2 * Math.PI),
        rng(2 * Math.PI),
        rng(2 * Math.PI)
    ];
    const speeds = [
        rng(t.options.speed[0], t.options.speed[1]) * dr(),
        rng(t.options.speed[0], t.options.speed[1]) * dr(),
        rng(t.options.speed[0], t.options.speed[1]) * dr(),
        rng(t.options.speed[0], t.options.speed[1]) * dr()
    ];
    const h = () => {
        e.push(LN({ angle: angles, speed: speeds }, t.color));
        if (e.length > t.options.width) {
            e = e.slice(1);
        }
    };
    const n = () => {
        const { ctx: i, radius: s, centerX: h, centerY: n, options: o } = t;
        const r = s / 3;
        const a = (o.rotation * Math.PI) / 360;
        const d = o.amplitude;

        i.strokeStyle = t.color;
        i.beginPath();
        for (const l of e) {
            const { angle: $ } = l;
            const wave1 = $[0] * d + a;
            const wave2 = $[3] * d + a;
            const wave3 = $[1] * d * 2;
            const wave4 = $[2] * d * 2;

            const u = h - s * Math.cos(wave1);
            const p = n - s * Math.sin(wave1);
            const w = h - r * Math.cos(wave3);
            const _ = n - r * Math.sin(wave3);
            const v = h + r * Math.cos(wave4);
            const b = n + r * Math.sin(wave4);
            const c = h + s * Math.cos(wave2);
            const g = n + s * Math.sin(wave2);

            i.moveTo(u, p);
            i.bezierCurveTo(w, _, v, b, c, g);
        }
        i.stroke();
    };
    return { lines: e, angle: angles, speed: speeds, update: h, draw: n };
}

额外建议

如果帧率仍未达标,可以尝试降低options.width的数值(当前为300),减少单帧绘制的贝塞尔曲线数量,这对Firefox的帧率提升效果显著。

内容的提问来源于stack exchange,提问作者axy_david

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 05:42:26